From ecfb29340519378347fa946f3ff5daa2c2c8fa41 Mon Sep 17 00:00:00 2001
From: kebernet The default user agent. It is not marked final so
+ * buggy java compiler will not write this string
+ * into all classes that reference it. http://tinyurl.com/64t5n points to https://rome.dev.java.net/
+ * Some servers ban user agents with "Java" in the name. Add a FetcherListener. The FetcherListener will receive an FetcherEvent when
+ * a Fetcher event (feed polled, retrieved, etc) occurs Remove a FetcherListener Is this fetcher using rfc3229 delta encoding? Turn on or off rfc3229 delta encoding See http://www.ietf.org/rfc/rfc3229.txt and http://bobwyman.pubsub.com/main/2004/09/using_rfc3229_w.html NOTE: This is experimental and feedback is welcome! The feed will only be set if the eventType is EVENT_TYPE_FEED_RETRIEVED The feed will only be set if the eventType is EVENT_TYPE_FEED_RETRIEVED Called when a fetcher event occurs Handles HTTP error codes. Combine the entries in two feeds into a single feed. The returned feed will have the same data as the newFeed parameter, with
+ * the entries from originalFeed appended to the end of its entries.
A very simple implementation of the {@link com.sun.syndication.fetcher.impl.FeedFetcherCache} interface.
+ * + *This implementation uses a HashMap to cache retrieved feeds. This implementation is + * most suitible for sort term (client aggregator?) use, as the memory usage will increase + * over time as the number of feeds in the cache increases.
+ * + * @author Nick Lothian + * + */ +public class HashMapFeedInfoCache implements FeedFetcherCache, Serializable { + private static final long serialVersionUID = -1594665619950916222L; + + static HashMapFeedInfoCache _instance; + + private Map infoCache; + + /** + *Constructor for HashMapFeedInfoCache
+ * + *Only use this if you want multiple instances of the cache. + * Usually getInstance() is more appropriate.
+ * + */ + public HashMapFeedInfoCache() { + setInfoCache(createInfoCache()); + } + + /** + * Get the global instance of the cache + * @return an implementation of FeedFetcherCache + */ + public static synchronized FeedFetcherCache getInstance() { + if (_instance == null) { + _instance = new HashMapFeedInfoCache(); + } + return _instance; + } + + protected Map createInfoCache() { + return (Collections.synchronizedMap(new HashMap())); + } + + + protected Object get(Object key) { + return getInfoCache().get(key); + } + + /** + * @see extensions.io.FeedFetcherCache#getFeedInfo(java.net.URL) + */ + public SyndFeedInfo getFeedInfo(URL feedUrl) { + return (SyndFeedInfo) get(feedUrl.toString()); + } + + protected void put(Object key, Object value) { + getInfoCache().put(key, value); + } + + /** + * @see extensions.io.FeedFetcherCache#setFeedInfo(java.net.URL, extensions.io.SyndFeedInfo) + */ + public void setFeedInfo(URL feedUrl, SyndFeedInfo syndFeedInfo) { + put(feedUrl.toString(), syndFeedInfo); + } + + protected synchronized final Map getInfoCache() { + return infoCache; + } + + /** + * The API of this class indicates that map must thread safe. In other + * words, be sure to wrap it in a synchronized map unless you know + * what you are doing. + * + * @param map the map to use as the info cache. + */ + protected synchronized final void setInfoCache(Map map) { + infoCache = map; + } + + /** + * @see com.sun.syndication.fetcher.impl.FeedFetcherCache#clear() + */ + public void clear() { + synchronized( infoCache ) { + infoCache.clear(); + } + } + + /** + * @see com.sun.syndication.fetcher.impl.FeedFetcherCache#remove(java.net.URL) + */ + public SyndFeedInfo remove( final URL url ) { + if( url == null ) return null; + + return (SyndFeedInfo) infoCache.remove( url.toString() ); + } + +} diff --git a/src/java/com/sun/syndication/fetcher/impl/HttpClientFeedFetcher.java b/src/java/com/sun/syndication/fetcher/impl/HttpClientFeedFetcher.java new file mode 100644 index 0000000..30517be --- /dev/null +++ b/src/java/com/sun/syndication/fetcher/impl/HttpClientFeedFetcher.java @@ -0,0 +1,353 @@ +/* + * Copyright 2004 Sun Microsystems, Inc. + * + * Licensed 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 com.sun.syndication.fetcher.impl; + +import java.io.IOException; +import java.io.InputStream; +import java.net.HttpURLConnection; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.zip.GZIPInputStream; + +import org.apache.commons.httpclient.Credentials; +import org.apache.commons.httpclient.Header; +import org.apache.commons.httpclient.HttpClient; +import org.apache.commons.httpclient.HttpException; +import org.apache.commons.httpclient.HttpMethod; +import org.apache.commons.httpclient.methods.GetMethod; +import org.apache.commons.httpclient.params.HttpClientParams; + +import com.sun.syndication.feed.synd.SyndFeed; +import com.sun.syndication.fetcher.FetcherEvent; +import com.sun.syndication.fetcher.FetcherException; +import com.sun.syndication.io.FeedException; +import com.sun.syndication.io.SyndFeedInput; +import com.sun.syndication.io.XmlReader; + +/** + * @author Nick Lothian + */ +public class HttpClientFeedFetcher extends AbstractFeedFetcher { + + private FeedFetcherCache feedInfoCache; + private CredentialSupplier credentialSupplier; + private volatile HttpClientMethodCallbackIntf httpClientMethodCallback; + private volatile HttpClientParams httpClientParams; + + public HttpClientFeedFetcher() { + super(); + setHttpClientParams(new HttpClientParams()); + } + + /** + * @param cache + */ + public HttpClientFeedFetcher(FeedFetcherCache cache) { + this(); + setFeedInfoCache(cache); + } + + + public HttpClientFeedFetcher(FeedFetcherCache cache, CredentialSupplier credentialSupplier) { + this(cache); + setCredentialSupplier(credentialSupplier); + } + + + /** + * @return Returns the httpClientParams. + */ + public synchronized HttpClientParams getHttpClientParams() { + return this.httpClientParams; + } + /** + * @param httpClientParams The httpClientParams to set. + */ + public synchronized void setHttpClientParams(HttpClientParams httpClientParams) { + this.httpClientParams = httpClientParams; + } + + /** + * @param timeout Sets the connect timeout for the HttpClient but using the URLConnection method name. + * Uses the HttpClientParams method setConnectionManagerTimeout instead of setConnectTimeout + * + */ + public synchronized void setConnectTimeout(int timeout) { + httpClientParams.setConnectionManagerTimeout(timeout); + } + + /** + * @return The currently used connect timeout for the HttpClient but using the URLConnection method name. + * Uses the HttpClientParams method getConnectionManagerTimeout instead of getConnectTimeout + * + */ + public int getConnectTimeout() { + return (int) this.getHttpClientParams().getConnectionManagerTimeout(); + } + + /** + * @return The currently used read timeout for the URLConnection, 0 is unlimited, i.e. no timeout + */ + public synchronized void setReadTimeout(int timeout) { + httpClientParams.setSoTimeout(timeout); + } + + /** + * @param timeout Sets the read timeout for the URLConnection to a specified timeout, in milliseconds. + */ + public int getReadTimeout() { + return (int) this.getHttpClientParams().getSoTimeout(); + } + + public HttpClientMethodCallbackIntf getHttpClientMethodCallback() { + return httpClientMethodCallback; + } + + public synchronized void setHttpClientMethodCallback(HttpClientMethodCallbackIntf httpClientMethodCallback) { + this.httpClientMethodCallback = httpClientMethodCallback; + } + + /** + * @return the feedInfoCache. + */ + public synchronized FeedFetcherCache getFeedInfoCache() { + return feedInfoCache; + } + + /** + * @param feedInfoCache the feedInfoCache to set + */ + public synchronized void setFeedInfoCache(FeedFetcherCache feedInfoCache) { + this.feedInfoCache = feedInfoCache; + } + + /** + * @return Returns the credentialSupplier. + */ + public synchronized CredentialSupplier getCredentialSupplier() { + return credentialSupplier; + } + /** + * @param credentialSupplier The credentialSupplier to set. + */ + public synchronized void setCredentialSupplier(CredentialSupplier credentialSupplier) { + this.credentialSupplier = credentialSupplier; + } + + /** + * @see com.sun.syndication.fetcher.FeedFetcher#retrieveFeed(java.net.URL) + */ + public SyndFeed retrieveFeed(URL feedUrl) throws IllegalArgumentException, IOException, FeedException, FetcherException { + if (feedUrl == null) { + throw new IllegalArgumentException("null is not a valid URL"); + } + // TODO Fix this + //System.setProperty("org.apache.commons.logging.Log", "org.apache.commons.logging.impl.SimpleLog"); + HttpClient client = new HttpClient(httpClientParams); + + if (getCredentialSupplier() != null) { + client.getState().setAuthenticationPreemptive(true); + // TODO what should realm be here? + Credentials credentials = getCredentialSupplier().getCredentials(null, feedUrl.getHost()); + if (credentials != null) { + client.getState().setCredentials(null, feedUrl.getHost(), credentials); + } + } + + + System.setProperty("httpclient.useragent", getUserAgent()); + String urlStr = feedUrl.toString(); + + HttpMethod method = new GetMethod(urlStr); + method.addRequestHeader("Accept-Encoding", "gzip"); + method.addRequestHeader("User-Agent", getUserAgent()); + method.setFollowRedirects(true); + + if (httpClientMethodCallback != null) { + synchronized (httpClientMethodCallback) { + httpClientMethodCallback.afterHttpClientMethodCreate(method); + } + } + + FeedFetcherCache cache = getFeedInfoCache(); + if (cache != null) { + // retrieve feed + + try { + if (isUsingDeltaEncoding()) { + method.setRequestHeader("A-IM", "feed"); + } + + // get the feed info from the cache + // Note that syndFeedInfo will be null if it is not in the cache + SyndFeedInfo syndFeedInfo = cache.getFeedInfo(feedUrl); + if (syndFeedInfo != null) { + method.setRequestHeader("If-None-Match", syndFeedInfo.getETag()); + + if (syndFeedInfo.getLastModified() instanceof String) { + method.setRequestHeader("If-Modified-Since", (String)syndFeedInfo.getLastModified()); + } + } + + int statusCode = client.executeMethod(method); + fireEvent(FetcherEvent.EVENT_TYPE_FEED_POLLED, urlStr); + handleErrorCodes(statusCode); + + SyndFeed feed = getFeed(syndFeedInfo, urlStr, method, statusCode); + + syndFeedInfo = buildSyndFeedInfo(feedUrl, urlStr, method, feed, statusCode); + + cache.setFeedInfo(new URL(urlStr), syndFeedInfo); + + // the feed may have been modified to pick up cached values + // (eg - for delta encoding) + feed = syndFeedInfo.getSyndFeed(); + + return feed; + } finally { + method.releaseConnection(); + method.recycle(); + } + + } else { + // cache is not in use + try { + int statusCode = client.executeMethod(method); + fireEvent(FetcherEvent.EVENT_TYPE_FEED_POLLED, urlStr); + handleErrorCodes(statusCode); + + return getFeed(null, urlStr, method, statusCode); + } finally { + method.releaseConnection(); + method.recycle(); + } + } + } + + + /** + * @param feedUrl + * @param urlStr + * @param method + * @param feed + * @return + * @throws MalformedURLException + */ + private SyndFeedInfo buildSyndFeedInfo(URL feedUrl, String urlStr, HttpMethod method, SyndFeed feed, int statusCode) throws MalformedURLException { + SyndFeedInfo syndFeedInfo; + syndFeedInfo = new SyndFeedInfo(); + + // this may be different to feedURL because of 3XX redirects + syndFeedInfo.setUrl(new URL(urlStr)); + syndFeedInfo.setId(feedUrl.toString()); + + Header imHeader = method.getResponseHeader("IM"); + if (imHeader != null && imHeader.getValue().indexOf("feed") >= 0 && isUsingDeltaEncoding()) { + FeedFetcherCache cache = getFeedInfoCache(); + if (cache != null && statusCode == 226) { + // client is setup to use http delta encoding and the server supports it and has returned a delta encoded response + // This response only includes new items + SyndFeedInfo cachedInfo = cache.getFeedInfo(feedUrl); + if (cachedInfo != null) { + SyndFeed cachedFeed = cachedInfo.getSyndFeed(); + + // set the new feed to be the orginal feed plus the new items + feed = combineFeeds(cachedFeed, feed); + } + } + } + + Header lastModifiedHeader = method.getResponseHeader("Last-Modified"); + if (lastModifiedHeader != null) { + syndFeedInfo.setLastModified(lastModifiedHeader.getValue()); + } + + Header eTagHeader = method.getResponseHeader("ETag"); + if (eTagHeader != null) { + syndFeedInfo.setETag(eTagHeader.getValue()); + } + + syndFeedInfo.setSyndFeed(feed); + + return syndFeedInfo; + } + + /** + * @param client + * @param urlStr + * @param method + * @return + * @throws IOException + * @throws HttpException + * @throws FetcherException + * @throws FeedException + */ + private SyndFeed retrieveFeed(String urlStr, HttpMethod method) throws IOException, HttpException, FetcherException, FeedException { + + InputStream stream = null; + if ((method.getResponseHeader("Content-Encoding") != null) && ("gzip".equalsIgnoreCase(method.getResponseHeader("Content-Encoding").getValue()))) { + stream = new GZIPInputStream(method.getResponseBodyAsStream()); + } else { + stream = method.getResponseBodyAsStream(); + } + try { + XmlReader reader = null; + if (method.getResponseHeader("Content-Type") != null) { + reader = new XmlReader(stream, method.getResponseHeader("Content-Type").getValue(), true); + } else { + reader = new XmlReader(stream, true); + } + SyndFeedInput syndFeedInput = new SyndFeedInput(); + syndFeedInput.setPreserveWireFeed(isPreserveWireFeed()); + + return syndFeedInput.build(reader); + } finally { + if (stream != null) { + stream.close(); + } + } + } + + private SyndFeed getFeed(SyndFeedInfo syndFeedInfo, String urlStr, HttpMethod method, int statusCode) throws IOException, HttpException, FetcherException, FeedException { + + if (statusCode == HttpURLConnection.HTTP_NOT_MODIFIED && syndFeedInfo != null) { + fireEvent(FetcherEvent.EVENT_TYPE_FEED_UNCHANGED, urlStr); + return syndFeedInfo.getSyndFeed(); + } + + SyndFeed feed = retrieveFeed(urlStr, method); + fireEvent(FetcherEvent.EVENT_TYPE_FEED_RETRIEVED, urlStr, feed); + return feed; + } + + public interface CredentialSupplier { + public Credentials getCredentials(String realm, String host); + } + + public interface HttpClientMethodCallbackIntf { + /** + * Allows access to the underlying HttpClient HttpMethod object. + * Note that in most cases, method.setRequestHeader(String, String) + * is what you want to do (rather than method.addRequestHeader(String, String)) + * + * @param method + */ + public void afterHttpClientMethodCreate(HttpMethod method); + } + +} diff --git a/src/java/com/sun/syndication/fetcher/impl/HttpURLFeedFetcher.java b/src/java/com/sun/syndication/fetcher/impl/HttpURLFeedFetcher.java new file mode 100644 index 0000000..c08def1 --- /dev/null +++ b/src/java/com/sun/syndication/fetcher/impl/HttpURLFeedFetcher.java @@ -0,0 +1,295 @@ +/* + * Copyright 2004 Sun Microsystems, Inc. + * + * Licensed 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 com.sun.syndication.fetcher.impl; + +import java.io.BufferedInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.HttpURLConnection; +import java.net.URL; +import java.net.URLConnection; +import java.util.zip.GZIPInputStream; + +import com.sun.syndication.feed.synd.SyndFeed; +import com.sun.syndication.fetcher.FetcherEvent; +import com.sun.syndication.fetcher.FetcherException; +import com.sun.syndication.io.FeedException; +import com.sun.syndication.io.SyndFeedInput; +import com.sun.syndication.io.XmlReader; + +/** + *Class to retrieve syndication files via HTTP.
+ * + *If passed a {@link com.sun.syndication.fetcher.impl.FeedFetcherCache} in the + * constructor it will use conditional gets to only retrieve modified content.
+ * + *The class uses the Accept-Encoding: gzip header to retrieve gzipped feeds where + * supported by the server.
+ * + *Simple usage: + *
+ * // create the cache + * FeedFetcherCache feedInfoCache = HashMapFeedInfoCache.getFeedInfoCache(); + * // retrieve the feed the first time + * // any subsequent request will use conditional gets and only + * // retrieve the resource if it has changed + * SyndFeed feed = new HttpURLFeedFetcher(feedInfoCache).retrieveFeed(feedUrl); + *+ * + * + * + * @see http://fishbowl.pastiche.org/2002/10/21/http_conditional_get_for_rss_hackers + * @see http://diveintomark.org/archives/2003/07/21/atom_aggregator_behavior_http_level + * @see http://bobwyman.pubsub.com/main/2004/09/using_rfc3229_w.html + * @author Nick Lothian + */ +public class HttpURLFeedFetcher extends AbstractFeedFetcher { + static final int POLL_EVENT = 1; + static final int RETRIEVE_EVENT = 2; + static final int UNCHANGED_EVENT = 3; + + private FeedFetcherCache feedInfoCache; + + + /** + * Constructor to use HttpURLFeedFetcher without caching of feeds + * + */ + public HttpURLFeedFetcher() { + super(); + } + + /** + * Constructor to enable HttpURLFeedFetcher to cache feeds + * + * @param feedCache - an instance of the FeedFetcherCache interface + */ + public HttpURLFeedFetcher(FeedFetcherCache feedInfoCache) { + this(); + setFeedInfoCache(feedInfoCache); + } + + /** + * Retrieve a feed over HTTP + * + * @param feedUrl A non-null URL of a RSS/Atom feed to retrieve + * @return A {@link com.sun.syndication.feed.synd.SyndFeed} object + * @throws IllegalArgumentException if the URL is null; + * @throws IOException if a TCP error occurs + * @throws FeedException if the feed is not valid + * @throws FetcherException if a HTTP error occurred + */ + public SyndFeed retrieveFeed(URL feedUrl) throws IllegalArgumentException, IOException, FeedException, FetcherException { + if (feedUrl == null) { + throw new IllegalArgumentException("null is not a valid URL"); + } + + URLConnection connection = feedUrl.openConnection(); + if (!(connection instanceof HttpURLConnection)) { + throw new IllegalArgumentException(feedUrl.toExternalForm() + " is not a valid HTTP Url"); + } + HttpURLConnection httpConnection = (HttpURLConnection)connection; + // httpConnection.setInstanceFollowRedirects(true); // this is true by default, but can be changed on a claswide basis + + FeedFetcherCache cache = getFeedInfoCache(); + if (cache != null) { + SyndFeedInfo syndFeedInfo = cache.getFeedInfo(feedUrl); + setRequestHeaders(connection, syndFeedInfo); + httpConnection.connect(); + try { + fireEvent(FetcherEvent.EVENT_TYPE_FEED_POLLED, connection); + + if (syndFeedInfo == null) { + // this is a feed that hasn't been retrieved + syndFeedInfo = new SyndFeedInfo(); + retrieveAndCacheFeed(feedUrl, syndFeedInfo, httpConnection); + } else { + // check the response code + int responseCode = httpConnection.getResponseCode(); + if (responseCode != HttpURLConnection.HTTP_NOT_MODIFIED) { + // the response code is not 304 NOT MODIFIED + // This is either because the feed server + // does not support condition gets + // or because the feed hasn't changed + retrieveAndCacheFeed(feedUrl, syndFeedInfo, httpConnection); + } else { + // the feed does not need retrieving + fireEvent(FetcherEvent.EVENT_TYPE_FEED_UNCHANGED, connection); + } + } + + return syndFeedInfo.getSyndFeed(); + } finally { + httpConnection.disconnect(); + } + } else { + fireEvent(FetcherEvent.EVENT_TYPE_FEED_POLLED, connection); + InputStream inputStream = null; + setRequestHeaders(connection, null); + httpConnection.connect(); + try { + inputStream = httpConnection.getInputStream(); + return getSyndFeedFromStream(inputStream, connection); + } catch (java.io.IOException e) { + handleErrorCodes(((HttpURLConnection)connection).getResponseCode()); + } finally { + if (inputStream != null) { + inputStream.close(); + } + httpConnection.disconnect(); + } + // we will never actually get to this line + return null; + } + } + + protected void retrieveAndCacheFeed(URL feedUrl, SyndFeedInfo syndFeedInfo, HttpURLConnection connection) throws IllegalArgumentException, FeedException, FetcherException, IOException { + handleErrorCodes(connection.getResponseCode()); + + resetFeedInfo(feedUrl, syndFeedInfo, connection); + FeedFetcherCache cache = getFeedInfoCache(); + // resetting feed info in the cache + // could be needed for some implementations + // of FeedFetcherCache (eg, distributed HashTables) + if (cache != null) { + cache.setFeedInfo(feedUrl, syndFeedInfo); + } + } + + protected void resetFeedInfo(URL orignalUrl, SyndFeedInfo syndFeedInfo, HttpURLConnection connection) throws IllegalArgumentException, IOException, FeedException { + // need to always set the URL because this may have changed due to 3xx redirects + syndFeedInfo.setUrl(connection.getURL()); + + // the ID is a persistant value that should stay the same even if the URL for the + // feed changes (eg, by 3xx redirects) + syndFeedInfo.setId(orignalUrl.toString()); + + // This will be 0 if the server doesn't support or isn't setting the last modified header + syndFeedInfo.setLastModified(new Long(connection.getLastModified())); + + // This will be null if the server doesn't support or isn't setting the ETag header + syndFeedInfo.setETag(connection.getHeaderField("ETag")); + + // get the contents + InputStream inputStream = null; + try { + inputStream = connection.getInputStream(); + SyndFeed syndFeed = getSyndFeedFromStream(inputStream, connection); + + String imHeader = connection.getHeaderField("IM"); + if (isUsingDeltaEncoding() && (imHeader!= null && imHeader.indexOf("feed") >= 0)) { + FeedFetcherCache cache = getFeedInfoCache(); + if (cache != null && connection.getResponseCode() == 226) { + // client is setup to use http delta encoding and the server supports it and has returned a delta encoded response + // This response only includes new items + SyndFeedInfo cachedInfo = cache.getFeedInfo(orignalUrl); + if (cachedInfo != null) { + SyndFeed cachedFeed = cachedInfo.getSyndFeed(); + + // set the new feed to be the orginal feed plus the new items + syndFeed = combineFeeds(cachedFeed, syndFeed); + } + } + } + + syndFeedInfo.setSyndFeed(syndFeed); + } finally { + if (inputStream != null) { + inputStream.close(); + } + } + } + + /** + *
Set appropriate HTTP headers, including conditional get and gzip encoding headers
+ * + * @param connection A URLConnection + * @param syndFeedInfo The SyndFeedInfo for the feed to be retrieved. May be null + */ + protected void setRequestHeaders(URLConnection connection, SyndFeedInfo syndFeedInfo) { + if (syndFeedInfo != null) { + // set the headers to get feed only if modified + // we support the use of both last modified and eTag headers + if (syndFeedInfo.getLastModified() != null) { + Object lastModified = syndFeedInfo.getLastModified(); + if (lastModified instanceof Long) { + connection.setIfModifiedSince(((Long)syndFeedInfo.getLastModified()).longValue()); + } + } + if (syndFeedInfo.getETag() != null) { + connection.setRequestProperty("If-None-Match", syndFeedInfo.getETag()); + } + + } + // header to retrieve feed gzipped + connection.setRequestProperty("Accept-Encoding", "gzip"); + + // set the user agent + connection.addRequestProperty("User-Agent", getUserAgent()); + + if (isUsingDeltaEncoding()) { + connection.addRequestProperty("A-IM", "feed"); + } + } + + private SyndFeed readSyndFeedFromStream(InputStream inputStream, URLConnection connection) throws IOException, IllegalArgumentException, FeedException { + BufferedInputStream is; + if ("gzip".equalsIgnoreCase(connection.getContentEncoding())) { + // handle gzip encoded content + is = new BufferedInputStream(new GZIPInputStream(inputStream)); + } else { + is = new BufferedInputStream(inputStream); + } + + //InputStreamReader reader = new InputStreamReader(is, ResponseHandler.getCharacterEncoding(connection)); + + //SyndFeedInput input = new SyndFeedInput(); + + XmlReader reader = null; + if (connection.getHeaderField("Content-Type") != null) { + reader = new XmlReader(is, connection.getHeaderField("Content-Type"), true); + } else { + reader = new XmlReader(is, true); + } + + SyndFeedInput syndFeedInput = new SyndFeedInput(); + syndFeedInput.setPreserveWireFeed(isPreserveWireFeed()); + + return syndFeedInput.build(reader); + + } + + private SyndFeed getSyndFeedFromStream(InputStream inputStream, URLConnection connection) throws IOException, IllegalArgumentException, FeedException { + SyndFeed feed = readSyndFeedFromStream(inputStream, connection); + fireEvent(FetcherEvent.EVENT_TYPE_FEED_RETRIEVED, connection, feed); + return feed; + } + + /** + * @return The FeedFetcherCache used by this fetcher (Could be null) + */ + public synchronized FeedFetcherCache getFeedInfoCache() { + return feedInfoCache; + } + + /** + * @param cache The cache to be used by this fetcher (pass null to stop using a cache) + */ + public synchronized void setFeedInfoCache(FeedFetcherCache cache) { + feedInfoCache = cache; + } +} diff --git a/src/java/com/sun/syndication/fetcher/impl/LinkedHashMapFeedInfoCache.java b/src/java/com/sun/syndication/fetcher/impl/LinkedHashMapFeedInfoCache.java new file mode 100644 index 0000000..736f10d --- /dev/null +++ b/src/java/com/sun/syndication/fetcher/impl/LinkedHashMapFeedInfoCache.java @@ -0,0 +1,70 @@ +package com.sun.syndication.fetcher.impl; + +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.Map; + +/** + *An implementation of the {@link com.sun.syndication.fetcher.impl.FeedFetcherCache} interface.
+ * + *Unlike the HashMapFeedInfoCache this implementation will not grow unbound
+ * + * @author Javier Kohen + * @author Nick Lothian + * + */ +public class LinkedHashMapFeedInfoCache extends HashMapFeedInfoCache { + private final class CacheImpl extends LinkedHashMap { + private static final long serialVersionUID = -6977191330127794920L; + + public CacheImpl() { + super(16, 0.75F, true); + } + + protected boolean removeEldestEntry(Map.Entry eldest) { + return size() > getMaxEntries(); + } + } + + private static final int DEFAULT_MAX_ENTRIES = 20; + + private static final long serialVersionUID = 1694228973357997417L; + + private int maxEntries = DEFAULT_MAX_ENTRIES; + + private final static LinkedHashMapFeedInfoCache _instance = new LinkedHashMapFeedInfoCache(); + + + /** + * Get the global instance of the cache + * @return an implementation of FeedFetcherCache + */ + public static final FeedFetcherCache getInstance() { + return _instance; + } + + /** + *Constructor for HashMapFeedInfoCache
+ * + *Only use this if you want multiple instances of the cache. + * Usually {@link #getInstance()} is more appropriate.
+ * + * @see #getInstance() + */ + public LinkedHashMapFeedInfoCache() { + super(); + } + + protected Map createInfoCache() { + return Collections.synchronizedMap(new CacheImpl()); + } + + public synchronized final int getMaxEntries() { + return maxEntries; + } + + public synchronized final void setMaxEntries(int maxEntries) { + this.maxEntries = maxEntries; + } + +} diff --git a/src/java/com/sun/syndication/fetcher/impl/ResponseHandler.java b/src/java/com/sun/syndication/fetcher/impl/ResponseHandler.java new file mode 100644 index 0000000..db877fe --- /dev/null +++ b/src/java/com/sun/syndication/fetcher/impl/ResponseHandler.java @@ -0,0 +1,57 @@ +/* + * Copyright 2004 Sun Microsystems, Inc. + * + * Licensed 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 com.sun.syndication.fetcher.impl; + +import java.net.URLConnection; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Utility class to help deal with HTTP responses + * + */ +public class ResponseHandler { + public static final String defaultCharacterEncoding = "ISO-8859-1"; + + private final static Pattern characterEncodingPattern = Pattern.compile("charset=([.[^; ]]*)"); + + public static String getCharacterEncoding(URLConnection connection) { + return getCharacterEncoding(connection.getContentType()); + } + + /** + * + *Gets the character encoding of a response. (Note that this is different to + * the content-encoding)
+ * + * @param contentTypeHeader the value of the content-type HTTP header eg: text/html; charset=ISO-8859-4 + * @return the character encoding, eg: ISO-8859-4 + */ + public static String getCharacterEncoding(String contentTypeHeader) { + if (contentTypeHeader == null) { + return defaultCharacterEncoding; + } + + Matcher m = characterEncodingPattern.matcher(contentTypeHeader); + //if (!m.matches()) { + if (!m.find()) { + return defaultCharacterEncoding; + } else { + return m.group(1); + } + } +} diff --git a/src/java/com/sun/syndication/fetcher/impl/SyndFeedInfo.java b/src/java/com/sun/syndication/fetcher/impl/SyndFeedInfo.java new file mode 100644 index 0000000..a19f5fe --- /dev/null +++ b/src/java/com/sun/syndication/fetcher/impl/SyndFeedInfo.java @@ -0,0 +1,149 @@ +/* + * Copyright 2004 Sun Microsystems, Inc. + * + * Licensed 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 com.sun.syndication.fetcher.impl; + +import java.io.Serializable; +import java.net.URL; + +import com.sun.syndication.feed.impl.ObjectBean; +import com.sun.syndication.feed.synd.SyndFeed; + +/** + *A class to represent a {@link com.sun.syndication.feed.synd.SyndFeed} + * and some useful information about it.
+ * + *This class is thread safe, as expected by the different feed fetcher + * implementations.
+ * + * @author Nick Lothian + */ +public class SyndFeedInfo implements Cloneable, Serializable { + private static final long serialVersionUID = -1874786860901426015L; + + private final ObjectBean _objBean; + private String id; + private URL url; + private Object lastModified; + private String eTag; + private SyndFeed syndFeed; + + public SyndFeedInfo() { + _objBean = new ObjectBean(this.getClass(),this); + } + + /** + * Creates a deep 'bean' clone of the object. + *+ * @return a clone of the object. + * @throws CloneNotSupportedException thrown if an element of the object cannot be cloned. + * + */ + public Object clone() throws CloneNotSupportedException { + return _objBean.clone(); + } + + /** + * Indicates whether some other object is "equal to" this one as defined by the Object equals() method. + *
+ * @param other he reference object with which to compare. + * @return true if 'this' object is equal to the 'other' object. + * + */ + public boolean equals(Object other) { + return _objBean.equals(other); + } + + /** + * Returns a hashcode value for the object. + *
+ * It follows the contract defined by the Object hashCode() method. + *
+ * @return the hashcode of the bean object. + * + */ + public int hashCode() { + return _objBean.hashCode(); + } + + /** + * Returns the String representation for the object. + *
+ * @return String representation for the object. + * + */ + public String toString() { + return _objBean.toString(); + } + + + /** + * @return the ETag the feed was last retrieved with + */ + public synchronized String getETag() { + return eTag; + } + + /** + * @return the last modified date for the feed + */ + public synchronized Object getLastModified() { + return lastModified; + } + + /** + * @return the URL the feed was served from + */ + public synchronized URL getUrl() { + return url; + } + + public synchronized void setETag(String string) { + eTag = string; + } + + public synchronized void setLastModified(Object o) { + lastModified = o; + } + + public synchronized void setUrl(URL url) { + this.url = url; + } + + public synchronized SyndFeed getSyndFeed() { + return syndFeed; + } + + public synchronized void setSyndFeed(SyndFeed feed) { + syndFeed = feed; + } + + /** + * @return A unique ID to identify the feed + */ + public synchronized String getId() { + return id; + } + + /** + * @param string A unique ID to identify the feed. Note that if the URL of the feed + * changes this will remain the same + */ + public synchronized void setId(String string) { + id = string; + } + +} diff --git a/src/java/com/sun/syndication/fetcher/samples/FeedAggregator.java b/src/java/com/sun/syndication/fetcher/samples/FeedAggregator.java new file mode 100644 index 0000000..05e160c --- /dev/null +++ b/src/java/com/sun/syndication/fetcher/samples/FeedAggregator.java @@ -0,0 +1,92 @@ +/* + * Copyright 2004 Sun Microsystems, Inc. + * + * Licensed 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 com.sun.syndication.fetcher.samples; + +import java.io.PrintWriter; +import java.net.URL; +import java.util.ArrayList; +import java.util.List; + +import com.sun.syndication.feed.synd.SyndFeedImpl; +import com.sun.syndication.feed.synd.SyndFeed; +import com.sun.syndication.fetcher.FeedFetcher; +import com.sun.syndication.fetcher.impl.FeedFetcherCache; +import com.sun.syndication.fetcher.impl.HashMapFeedInfoCache; +import com.sun.syndication.fetcher.impl.HttpURLFeedFetcher; +import com.sun.syndication.io.SyndFeedOutput; + +/** + *
It aggregates a list of RSS/Atom feeds (they can be of different types) + * into a single feed of the specified type.
+ * + *Converted from the original FeedAggregator sample
+ * + * @author Alejandro Abdelnur + * @author Nick Lothian + * + */ +public class FeedAggregator { + + public static void main(String[] args) { + boolean ok = false; + if (args.length>=2) { + try { + String outputType = args[0]; + + SyndFeed feed = new SyndFeedImpl(); + feed.setFeedType(outputType); + + feed.setTitle("Aggregated Feed"); + feed.setDescription("Anonymous Aggregated Feed"); + feed.setAuthor("anonymous"); + feed.setLink("http://www.anonymous.com"); + + List entries = new ArrayList(); + feed.setEntries(entries); + + FeedFetcherCache feedInfoCache = HashMapFeedInfoCache.getInstance(); + FeedFetcher feedFetcher = new HttpURLFeedFetcher(feedInfoCache); + + for (int i=1;iThe default user agent. It is not marked final so - * buggy java compiler will not write this string - * into all classes that reference it.
- * - *http://tinyurl.com/64t5n points to https://rome.dev.java.net/ - * Some servers ban user agents with "Java" in the name.
- * - */ - public static String DEFAULT_USER_AGENT = "Rome Client (http://tinyurl.com/64t5n)"; - /** - * @return the User-Agent currently being sent to servers - */ - public abstract String getUserAgent(); - /** - * @param string The User-Agent to sent to servers - */ - public abstract void setUserAgent(String string); - /** - * Retrieve a feed over HTTP - * - * @param feedUrl A non-null URL of a RSS/Atom feed to retrieve - * @return A {@link com.sun.syndication.feed.synd.SyndFeed} object - * @throws IllegalArgumentException if the URL is null; - * @throws IOException if a TCP error occurs - * @throws FeedException if the feed is not valid - * @throws FetcherException if a HTTP error occurred - */ - public abstract SyndFeed retrieveFeed(URL feedUrl) throws IllegalArgumentException, IOException, FeedException, FetcherException; + /** + *The default user agent. It is not marked final so + * buggy java compiler will not write this string + * into all classes that reference it.
+ * + *http://tinyurl.com/64t5n points to https://rome.dev.java.net/ + * Some servers ban user agents with "Java" in the name.
+ * + */ + public static String DEFAULT_USER_AGENT = "Rome Client (http://tinyurl.com/64t5n)"; - /** - *Add a FetcherListener.
- * - *The FetcherListener will receive an FetcherEvent when - * a Fetcher event (feed polled, retrieved, etc) occurs
- * - * @param listener The FetcherListener to recieve the event - */ - public abstract void addFetcherEventListener(FetcherListener listener); + /** + * @return the User-Agent currently being sent to servers + */ + public abstract String getUserAgent(); - /** - *Remove a FetcherListener
- * - * @param listener The FetcherListener to remove - */ - public abstract void removeFetcherEventListener(FetcherListener listener); + /** + * @param string The User-Agent to sent to servers + */ + public abstract void setUserAgent(String string); - /** - *Is this fetcher using rfc3229 delta encoding?
- * - * @return - */ + /** + * Retrieve a feed over HTTP + * + * @param feedUrl A non-null URL of a RSS/Atom feed to retrieve + * @return A {@link com.sun.syndication.feed.synd.SyndFeed} object + * @throws IllegalArgumentException if the URL is null; + * @throws IOException if a TCP error occurs + * @throws FeedException if the feed is not valid + * @throws FetcherException if a HTTP error occurred + */ + public abstract SyndFeed retrieveFeed(URL feedUrl) throws IllegalArgumentException, IOException, FeedException, FetcherException; + + /** + *Add a FetcherListener.
+ * + *The FetcherListener will receive an FetcherEvent when + * a Fetcher event (feed polled, retrieved, etc) occurs
+ * + * @param listener The FetcherListener to recieve the event + */ + public abstract void addFetcherEventListener(FetcherListener listener); + + /** + *Remove a FetcherListener
+ * + * @param listener The FetcherListener to remove + */ + public abstract void removeFetcherEventListener(FetcherListener listener); + + /** + *Is this fetcher using rfc3229 delta encoding?
+ * + * @return + */ public abstract boolean isUsingDeltaEncoding(); /** @@ -88,11 +91,10 @@ public interface FeedFetcher { * @param useDeltaEncoding */ public abstract void setUsingDeltaEncoding(boolean useDeltaEncoding); - + /** * If set to true, the WireFeed will be made accessible from the SyndFeed object returned from the Fetcher * via the originalWireFeed() method. Each Entry in the feed will have the corresponding wireEntry property set. */ void setPreserveWireFeed(boolean preserveWireFeed); - -} \ No newline at end of file +} From 9a8c0fd5ac96b6d95f302302f51f40f97266f373 Mon Sep 17 00:00:00 2001 From: kebernetThe default user agent. It is not marked final so * buggy java compiler will not write this string * into all classes that reference it.
* - *http://tinyurl.com/64t5n points to https://rome.dev.java.net/ + *
http://tinyurl.com/64t5n points to https://rome.dev.java.net * Some servers ban user agents with "Java" in the name.
* */ public static String DEFAULT_USER_AGENT = "Rome Client (http://tinyurl.com/64t5n)"; - /** - * @return the User-Agent currently being sent to servers - */ - public abstract String getUserAgent(); - /** * @param string The User-Agent to sent to servers */ public abstract void setUserAgent(String string); /** - * Retrieve a feed over HTTP - * - * @param feedUrl A non-null URL of a RSS/Atom feed to retrieve - * @return A {@link com.sun.syndication.feed.synd.SyndFeed} object - * @throws IllegalArgumentException if the URL is null; - * @throws IOException if a TCP error occurs - * @throws FeedException if the feed is not valid - * @throws FetcherException if a HTTP error occurred + * @return the User-Agent currently being sent to servers */ - public abstract SyndFeed retrieveFeed(URL feedUrl) throws IllegalArgumentException, IOException, FeedException, FetcherException; + public abstract String getUserAgent(); + + /** + *Turn on or off rfc3229 delta encoding
+ * + *See http://www.ietf.org/rfc/rfc3229.txt and http://bobwyman.pubsub.com/main/2004/09/using_rfc3229_w.html
+ * + *NOTE: This is experimental and feedback is welcome!
+ * + * @param useDeltaEncoding + */ + public abstract void setUsingDeltaEncoding(boolean useDeltaEncoding); + + /** + *Is this fetcher using rfc3229 delta encoding?
+ * + * @return + */ + public abstract boolean isUsingDeltaEncoding(); /** *Add a FetcherListener.
@@ -75,25 +82,23 @@ public interface FeedFetcher { public abstract void removeFetcherEventListener(FetcherListener listener); /** - *Is this fetcher using rfc3229 delta encoding?
+ * Retrieve a feed over HTTP * - * @return + * @param feedUrl A non-null URL of a RSS/Atom feed to retrieve + * @return A {@link com.sun.syndication.feed.synd.SyndFeed} object + * @throws IllegalArgumentException if the URL is null; + * @throws IOException if a TCP error occurs + * @throws FeedException if the feed is not valid + * @throws FetcherException if a HTTP error occurred */ - public abstract boolean isUsingDeltaEncoding(); + public abstract SyndFeed retrieveFeed(URL feedUrl) + throws IllegalArgumentException, IOException, FeedException, FetcherException; + + public SyndFeed retrieveFeed(String userAgent, URL url) + throws IllegalArgumentException, IOException, FeedException, FetcherException; /** - *Turn on or off rfc3229 delta encoding
- * - *See http://www.ietf.org/rfc/rfc3229.txt and http://bobwyman.pubsub.com/main/2004/09/using_rfc3229_w.html
- * - *NOTE: This is experimental and feedback is welcome!
- * - * @param useDeltaEncoding - */ - public abstract void setUsingDeltaEncoding(boolean useDeltaEncoding); - - /** - * If set to true, the WireFeed will be made accessible from the SyndFeed object returned from the Fetcher + * If set to true, the WireFeed will be made accessible from the SyndFeed object returned from the Fetcher * via the originalWireFeed() method. Each Entry in the feed will have the corresponding wireEntry property set. */ void setPreserveWireFeed(boolean preserveWireFeed); diff --git a/src/java/com/sun/syndication/fetcher/impl/HttpClientFeedFetcher.java b/src/java/com/sun/syndication/fetcher/impl/HttpClientFeedFetcher.java index 30517be..032c2da 100644 --- a/src/java/com/sun/syndication/fetcher/impl/HttpClientFeedFetcher.java +++ b/src/java/com/sun/syndication/fetcher/impl/HttpClientFeedFetcher.java @@ -14,15 +14,14 @@ * limitations under the License. * */ - package com.sun.syndication.fetcher.impl; -import java.io.IOException; -import java.io.InputStream; -import java.net.HttpURLConnection; -import java.net.MalformedURLException; -import java.net.URL; -import java.util.zip.GZIPInputStream; +import com.sun.syndication.feed.synd.SyndFeed; +import com.sun.syndication.fetcher.FetcherEvent; +import com.sun.syndication.fetcher.FetcherException; +import com.sun.syndication.io.FeedException; +import com.sun.syndication.io.SyndFeedInput; +import com.sun.syndication.io.XmlReader; import org.apache.commons.httpclient.Credentials; import org.apache.commons.httpclient.Header; @@ -32,322 +31,349 @@ import org.apache.commons.httpclient.HttpMethod; import org.apache.commons.httpclient.methods.GetMethod; import org.apache.commons.httpclient.params.HttpClientParams; -import com.sun.syndication.feed.synd.SyndFeed; -import com.sun.syndication.fetcher.FetcherEvent; -import com.sun.syndication.fetcher.FetcherException; -import com.sun.syndication.io.FeedException; -import com.sun.syndication.io.SyndFeedInput; -import com.sun.syndication.io.XmlReader; +import java.io.IOException; +import java.io.InputStream; + +import java.net.HttpURLConnection; +import java.net.MalformedURLException; +import java.net.URL; + +import java.util.zip.GZIPInputStream; + /** * @author Nick Lothian */ public class HttpClientFeedFetcher extends AbstractFeedFetcher { - - private FeedFetcherCache feedInfoCache; private CredentialSupplier credentialSupplier; + private FeedFetcherCache feedInfoCache; private volatile HttpClientMethodCallbackIntf httpClientMethodCallback; - private volatile HttpClientParams httpClientParams; + private volatile HttpClientParams httpClientParams; - public HttpClientFeedFetcher() { - super(); - setHttpClientParams(new HttpClientParams()); - } - - /** - * @param cache - */ - public HttpClientFeedFetcher(FeedFetcherCache cache) { - this(); - setFeedInfoCache(cache); - } - - - public HttpClientFeedFetcher(FeedFetcherCache cache, CredentialSupplier credentialSupplier) { - this(cache); - setCredentialSupplier(credentialSupplier); - } - - - /** - * @return Returns the httpClientParams. - */ - public synchronized HttpClientParams getHttpClientParams() { - return this.httpClientParams; + public HttpClientFeedFetcher() { + super(); + setHttpClientParams(new HttpClientParams()); } + /** - * @param httpClientParams The httpClientParams to set. + * @param cache */ - public synchronized void setHttpClientParams(HttpClientParams httpClientParams) { - this.httpClientParams = httpClientParams; - } + public HttpClientFeedFetcher(FeedFetcherCache cache) { + this(); + setFeedInfoCache(cache); + } + + public HttpClientFeedFetcher(FeedFetcherCache cache, CredentialSupplier credentialSupplier) { + this(cache); + setCredentialSupplier(credentialSupplier); + } /** * @param timeout Sets the connect timeout for the HttpClient but using the URLConnection method name. - * Uses the HttpClientParams method setConnectionManagerTimeout instead of setConnectTimeout - * - */ - public synchronized void setConnectTimeout(int timeout) { - httpClientParams.setConnectionManagerTimeout(timeout); - } - - /** - * @return The currently used connect timeout for the HttpClient but using the URLConnection method name. - * Uses the HttpClientParams method getConnectionManagerTimeout instead of getConnectTimeout - * - */ - public int getConnectTimeout() { - return (int) this.getHttpClientParams().getConnectionManagerTimeout(); - } - - /** - * @return The currently used read timeout for the URLConnection, 0 is unlimited, i.e. no timeout - */ - public synchronized void setReadTimeout(int timeout) { - httpClientParams.setSoTimeout(timeout); - } - - /** - * @param timeout Sets the read timeout for the URLConnection to a specified timeout, in milliseconds. - */ - public int getReadTimeout() { - return (int) this.getHttpClientParams().getSoTimeout(); - } - - public HttpClientMethodCallbackIntf getHttpClientMethodCallback() { - return httpClientMethodCallback; - } - - public synchronized void setHttpClientMethodCallback(HttpClientMethodCallbackIntf httpClientMethodCallback) { - this.httpClientMethodCallback = httpClientMethodCallback; - } - - /** - * @return the feedInfoCache. - */ - public synchronized FeedFetcherCache getFeedInfoCache() { - return feedInfoCache; - } - - /** - * @param feedInfoCache the feedInfoCache to set - */ - public synchronized void setFeedInfoCache(FeedFetcherCache feedInfoCache) { - this.feedInfoCache = feedInfoCache; - } - - /** - * @return Returns the credentialSupplier. - */ - public synchronized CredentialSupplier getCredentialSupplier() { - return credentialSupplier; + * Uses the HttpClientParams method setConnectionManagerTimeout instead of setConnectTimeout + * + */ + public synchronized void setConnectTimeout(int timeout) { + httpClientParams.setConnectionManagerTimeout(timeout); } + + /** + * @return The currently used connect timeout for the HttpClient but using the URLConnection method name. + * Uses the HttpClientParams method getConnectionManagerTimeout instead of getConnectTimeout + * + */ + public int getConnectTimeout() { + return (int) this.getHttpClientParams() + .getConnectionManagerTimeout(); + } + /** * @param credentialSupplier The credentialSupplier to set. */ public synchronized void setCredentialSupplier(CredentialSupplier credentialSupplier) { this.credentialSupplier = credentialSupplier; - } - - /** - * @see com.sun.syndication.fetcher.FeedFetcher#retrieveFeed(java.net.URL) - */ - public SyndFeed retrieveFeed(URL feedUrl) throws IllegalArgumentException, IOException, FeedException, FetcherException { - if (feedUrl == null) { - throw new IllegalArgumentException("null is not a valid URL"); - } - // TODO Fix this - //System.setProperty("org.apache.commons.logging.Log", "org.apache.commons.logging.impl.SimpleLog"); - HttpClient client = new HttpClient(httpClientParams); - - if (getCredentialSupplier() != null) { - client.getState().setAuthenticationPreemptive(true); - // TODO what should realm be here? - Credentials credentials = getCredentialSupplier().getCredentials(null, feedUrl.getHost()); - if (credentials != null) { - client.getState().setCredentials(null, feedUrl.getHost(), credentials); - } - } - - - System.setProperty("httpclient.useragent", getUserAgent()); - String urlStr = feedUrl.toString(); - - HttpMethod method = new GetMethod(urlStr); - method.addRequestHeader("Accept-Encoding", "gzip"); - method.addRequestHeader("User-Agent", getUserAgent()); - method.setFollowRedirects(true); - - if (httpClientMethodCallback != null) { - synchronized (httpClientMethodCallback) { - httpClientMethodCallback.afterHttpClientMethodCreate(method); - } - } - - FeedFetcherCache cache = getFeedInfoCache(); - if (cache != null) { - // retrieve feed + } - try { - if (isUsingDeltaEncoding()) { - method.setRequestHeader("A-IM", "feed"); - } + /** + * @return Returns the credentialSupplier. + */ + public synchronized CredentialSupplier getCredentialSupplier() { + return credentialSupplier; + } - // get the feed info from the cache - // Note that syndFeedInfo will be null if it is not in the cache - SyndFeedInfo syndFeedInfo = cache.getFeedInfo(feedUrl); - if (syndFeedInfo != null) { - method.setRequestHeader("If-None-Match", syndFeedInfo.getETag()); - - if (syndFeedInfo.getLastModified() instanceof String) { - method.setRequestHeader("If-Modified-Since", (String)syndFeedInfo.getLastModified()); - } - } - - int statusCode = client.executeMethod(method); - fireEvent(FetcherEvent.EVENT_TYPE_FEED_POLLED, urlStr); - handleErrorCodes(statusCode); - - SyndFeed feed = getFeed(syndFeedInfo, urlStr, method, statusCode); - - syndFeedInfo = buildSyndFeedInfo(feedUrl, urlStr, method, feed, statusCode); - - cache.setFeedInfo(new URL(urlStr), syndFeedInfo); - - // the feed may have been modified to pick up cached values - // (eg - for delta encoding) - feed = syndFeedInfo.getSyndFeed(); - - return feed; - } finally { - method.releaseConnection(); - method.recycle(); - } - - } else { - // cache is not in use - try { - int statusCode = client.executeMethod(method); - fireEvent(FetcherEvent.EVENT_TYPE_FEED_POLLED, urlStr); - handleErrorCodes(statusCode); - - return getFeed(null, urlStr, method, statusCode); - } finally { - method.releaseConnection(); - method.recycle(); - } - } - } + /** + * @param feedInfoCache the feedInfoCache to set + */ + public synchronized void setFeedInfoCache(FeedFetcherCache feedInfoCache) { + this.feedInfoCache = feedInfoCache; + } - - /** - * @param feedUrl - * @param urlStr - * @param method - * @param feed - * @return - * @throws MalformedURLException + /** + * @return the feedInfoCache. */ - private SyndFeedInfo buildSyndFeedInfo(URL feedUrl, String urlStr, HttpMethod method, SyndFeed feed, int statusCode) throws MalformedURLException { + public synchronized FeedFetcherCache getFeedInfoCache() { + return feedInfoCache; + } + + public synchronized void setHttpClientMethodCallback(HttpClientMethodCallbackIntf httpClientMethodCallback) { + this.httpClientMethodCallback = httpClientMethodCallback; + } + + public HttpClientMethodCallbackIntf getHttpClientMethodCallback() { + return httpClientMethodCallback; + } + + /** + * @param httpClientParams The httpClientParams to set. + */ + public synchronized void setHttpClientParams(HttpClientParams httpClientParams) { + this.httpClientParams = httpClientParams; + } + + /** + * @return Returns the httpClientParams. + */ + public synchronized HttpClientParams getHttpClientParams() { + return this.httpClientParams; + } + + /** + * @return The currently used read timeout for the URLConnection, 0 is unlimited, i.e. no timeout + */ + public synchronized void setReadTimeout(int timeout) { + httpClientParams.setSoTimeout(timeout); + } + + /** + * @param timeout Sets the read timeout for the URLConnection to a specified timeout, in milliseconds. + */ + public int getReadTimeout() { + return (int) this.getHttpClientParams() + .getSoTimeout(); + } + + public SyndFeed retrieveFeed(URL url) throws IllegalArgumentException, IOException, FeedException, FetcherException { + return this.retrieveFeed(this.getUserAgent(), url); + } + + /** + * @see com.sun.syndication.fetcher.FeedFetcher#retrieveFeed(java.net.URL) + */ + public SyndFeed retrieveFeed(String userAgent, URL feedUrl) + throws IllegalArgumentException, IOException, FeedException, FetcherException { + if (feedUrl == null) { + throw new IllegalArgumentException("null is not a valid URL"); + } + + // TODO Fix this + //System.setProperty("org.apache.commons.logging.Log", "org.apache.commons.logging.impl.SimpleLog"); + HttpClient client = new HttpClient(httpClientParams); + + if (getCredentialSupplier() != null) { + client.getState() + .setAuthenticationPreemptive(true); + + // TODO what should realm be here? + Credentials credentials = getCredentialSupplier() + .getCredentials(null, feedUrl.getHost()); + + if (credentials != null) { + client.getState() + .setCredentials(null, feedUrl.getHost(), credentials); + } + } + + System.setProperty("httpclient.useragent", getUserAgent()); + + String urlStr = feedUrl.toString(); + + HttpMethod method = new GetMethod(urlStr); + method.addRequestHeader("Accept-Encoding", "gzip"); + method.addRequestHeader("User-Agent", userAgent); + method.setFollowRedirects(true); + + if (httpClientMethodCallback != null) { + synchronized (httpClientMethodCallback) { + httpClientMethodCallback.afterHttpClientMethodCreate(method); + } + } + + FeedFetcherCache cache = getFeedInfoCache(); + + if (cache != null) { + // retrieve feed + try { + if (isUsingDeltaEncoding()) { + method.setRequestHeader("A-IM", "feed"); + } + + // get the feed info from the cache + // Note that syndFeedInfo will be null if it is not in the cache + SyndFeedInfo syndFeedInfo = cache.getFeedInfo(feedUrl); + + if (syndFeedInfo != null) { + method.setRequestHeader("If-None-Match", syndFeedInfo.getETag()); + + if (syndFeedInfo.getLastModified() instanceof String) { + method.setRequestHeader("If-Modified-Since", (String) syndFeedInfo.getLastModified()); + } + } + + int statusCode = client.executeMethod(method); + fireEvent(FetcherEvent.EVENT_TYPE_FEED_POLLED, urlStr); + handleErrorCodes(statusCode); + + SyndFeed feed = getFeed(syndFeedInfo, urlStr, method, statusCode); + + syndFeedInfo = buildSyndFeedInfo(feedUrl, urlStr, method, feed, statusCode); + + cache.setFeedInfo(new URL(urlStr), syndFeedInfo); + + // the feed may have been modified to pick up cached values + // (eg - for delta encoding) + feed = syndFeedInfo.getSyndFeed(); + + return feed; + } finally { + method.releaseConnection(); + method.recycle(); + } + } else { + // cache is not in use + try { + int statusCode = client.executeMethod(method); + fireEvent(FetcherEvent.EVENT_TYPE_FEED_POLLED, urlStr); + handleErrorCodes(statusCode); + + return getFeed(null, urlStr, method, statusCode); + } finally { + method.releaseConnection(); + method.recycle(); + } + } + } + + private SyndFeed getFeed(SyndFeedInfo syndFeedInfo, String urlStr, HttpMethod method, int statusCode) + throws IOException, HttpException, FetcherException, FeedException { + if ((statusCode == HttpURLConnection.HTTP_NOT_MODIFIED) && (syndFeedInfo != null)) { + fireEvent(FetcherEvent.EVENT_TYPE_FEED_UNCHANGED, urlStr); + + return syndFeedInfo.getSyndFeed(); + } + + SyndFeed feed = retrieveFeed(urlStr, method); + fireEvent(FetcherEvent.EVENT_TYPE_FEED_RETRIEVED, urlStr, feed); + + return feed; + } + + /** + * @param feedUrl + * @param urlStr + * @param method + * @param feed + * @return + * @throws MalformedURLException + */ + private SyndFeedInfo buildSyndFeedInfo(URL feedUrl, String urlStr, HttpMethod method, SyndFeed feed, int statusCode) + throws MalformedURLException { SyndFeedInfo syndFeedInfo; syndFeedInfo = new SyndFeedInfo(); - + // this may be different to feedURL because of 3XX redirects syndFeedInfo.setUrl(new URL(urlStr)); - syndFeedInfo.setId(feedUrl.toString()); - + syndFeedInfo.setId(feedUrl.toString()); + Header imHeader = method.getResponseHeader("IM"); - if (imHeader != null && imHeader.getValue().indexOf("feed") >= 0 && isUsingDeltaEncoding()) { - FeedFetcherCache cache = getFeedInfoCache(); - if (cache != null && statusCode == 226) { - // client is setup to use http delta encoding and the server supports it and has returned a delta encoded response - // This response only includes new items - SyndFeedInfo cachedInfo = cache.getFeedInfo(feedUrl); - if (cachedInfo != null) { - SyndFeed cachedFeed = cachedInfo.getSyndFeed(); - - // set the new feed to be the orginal feed plus the new items - feed = combineFeeds(cachedFeed, feed); - } - } - } - + + if ((imHeader != null) && (imHeader.getValue() + .indexOf("feed") >= 0) && isUsingDeltaEncoding()) { + FeedFetcherCache cache = getFeedInfoCache(); + + if ((cache != null) && (statusCode == 226)) { + // client is setup to use http delta encoding and the server supports it and has returned a delta encoded response + // This response only includes new items + SyndFeedInfo cachedInfo = cache.getFeedInfo(feedUrl); + + if (cachedInfo != null) { + SyndFeed cachedFeed = cachedInfo.getSyndFeed(); + + // set the new feed to be the orginal feed plus the new items + feed = combineFeeds(cachedFeed, feed); + } + } + } + Header lastModifiedHeader = method.getResponseHeader("Last-Modified"); + if (lastModifiedHeader != null) { syndFeedInfo.setLastModified(lastModifiedHeader.getValue()); } - + Header eTagHeader = method.getResponseHeader("ETag"); + if (eTagHeader != null) { syndFeedInfo.setETag(eTagHeader.getValue()); } - + syndFeedInfo.setSyndFeed(feed); - + return syndFeedInfo; } /** - * @param client - * @param urlStr - * @param method - * @return - * @throws IOException - * @throws HttpException - * @throws FetcherException - * @throws FeedException - */ - private SyndFeed retrieveFeed(String urlStr, HttpMethod method) throws IOException, HttpException, FetcherException, FeedException { - - InputStream stream = null; - if ((method.getResponseHeader("Content-Encoding") != null) && ("gzip".equalsIgnoreCase(method.getResponseHeader("Content-Encoding").getValue()))) { - stream = new GZIPInputStream(method.getResponseBodyAsStream()); - } else { - stream = method.getResponseBodyAsStream(); - } - try { - XmlReader reader = null; - if (method.getResponseHeader("Content-Type") != null) { - reader = new XmlReader(stream, method.getResponseHeader("Content-Type").getValue(), true); - } else { - reader = new XmlReader(stream, true); - } - SyndFeedInput syndFeedInput = new SyndFeedInput(); - syndFeedInput.setPreserveWireFeed(isPreserveWireFeed()); - - return syndFeedInput.build(reader); - } finally { - if (stream != null) { - stream.close(); - } - } - } + * @param client + * @param urlStr + * @param method + * @return + * @throws IOException + * @throws HttpException + * @throws FetcherException + * @throws FeedException + */ + private SyndFeed retrieveFeed(String urlStr, HttpMethod method) + throws IOException, HttpException, FetcherException, FeedException { + InputStream stream = null; - private SyndFeed getFeed(SyndFeedInfo syndFeedInfo, String urlStr, HttpMethod method, int statusCode) throws IOException, HttpException, FetcherException, FeedException { + if ((method.getResponseHeader("Content-Encoding") != null) && + ("gzip".equalsIgnoreCase(method.getResponseHeader("Content-Encoding").getValue()))) { + stream = new GZIPInputStream(method.getResponseBodyAsStream()); + } else { + stream = method.getResponseBodyAsStream(); + } + + try { + XmlReader reader = null; + + if (method.getResponseHeader("Content-Type") != null) { + reader = new XmlReader(stream, method.getResponseHeader("Content-Type").getValue(), true); + } else { + reader = new XmlReader(stream, true); + } + + SyndFeedInput syndFeedInput = new SyndFeedInput(); + syndFeedInput.setPreserveWireFeed(isPreserveWireFeed()); + + return syndFeedInput.build(reader); + } finally { + if (stream != null) { + stream.close(); + } + } + } - if (statusCode == HttpURLConnection.HTTP_NOT_MODIFIED && syndFeedInfo != null) { - fireEvent(FetcherEvent.EVENT_TYPE_FEED_UNCHANGED, urlStr); - return syndFeedInfo.getSyndFeed(); - } - - SyndFeed feed = retrieveFeed(urlStr, method); - fireEvent(FetcherEvent.EVENT_TYPE_FEED_RETRIEVED, urlStr, feed); - return feed; - } - public interface CredentialSupplier { public Credentials getCredentials(String realm, String host); } - - public interface HttpClientMethodCallbackIntf { - /** - * Allows access to the underlying HttpClient HttpMethod object. - * Note that in most cases, method.setRequestHeader(String, String) - * is what you want to do (rather than method.addRequestHeader(String, String)) - * - * @param method - */ - public void afterHttpClientMethodCreate(HttpMethod method); - } + public interface HttpClientMethodCallbackIntf { + /** + * Allows access to the underlying HttpClient HttpMethod object. + * Note that in most cases, method.setRequestHeader(String, String) + * is what you want to do (rather than method.addRequestHeader(String, String)) + * + * @param method + */ + public void afterHttpClientMethodCreate(HttpMethod method); + } } diff --git a/src/java/com/sun/syndication/fetcher/impl/HttpURLFeedFetcher.java b/src/java/com/sun/syndication/fetcher/impl/HttpURLFeedFetcher.java index c08def1..0f1f193 100644 --- a/src/java/com/sun/syndication/fetcher/impl/HttpURLFeedFetcher.java +++ b/src/java/com/sun/syndication/fetcher/impl/HttpURLFeedFetcher.java @@ -83,6 +83,10 @@ public class HttpURLFeedFetcher extends AbstractFeedFetcher { setFeedInfoCache(feedInfoCache); } + public SyndFeed retrieveFeed(URL feedUrl) throws IllegalArgumentException, IOException, FeedException, FetcherException { + return this.retrieveFeed(this.getUserAgent(), feedUrl); + } + /** * Retrieve a feed over HTTP * @@ -93,7 +97,7 @@ public class HttpURLFeedFetcher extends AbstractFeedFetcher { * @throws FeedException if the feed is not valid * @throws FetcherException if a HTTP error occurred */ - public SyndFeed retrieveFeed(URL feedUrl) throws IllegalArgumentException, IOException, FeedException, FetcherException { + public SyndFeed retrieveFeed(String userAgent, URL feedUrl) throws IllegalArgumentException, IOException, FeedException, FetcherException { if (feedUrl == null) { throw new IllegalArgumentException("null is not a valid URL"); } @@ -140,6 +144,9 @@ public class HttpURLFeedFetcher extends AbstractFeedFetcher { fireEvent(FetcherEvent.EVENT_TYPE_FEED_POLLED, connection); InputStream inputStream = null; setRequestHeaders(connection, null); + + connection.addRequestProperty("User-Agent", userAgent); + httpConnection.connect(); try { inputStream = httpConnection.getInputStream(); @@ -238,9 +245,6 @@ public class HttpURLFeedFetcher extends AbstractFeedFetcher { // header to retrieve feed gzipped connection.setRequestProperty("Accept-Encoding", "gzip"); - // set the user agent - connection.addRequestProperty("User-Agent", getUserAgent()); - if (isUsingDeltaEncoding()) { connection.addRequestProperty("A-IM", "feed"); } From 57349ed2c3857c45d0a2bc600b79fe2857e8e72d Mon Sep 17 00:00:00 2001 From: kebernetThe default user agent. It is not marked final so - * buggy java compiler will not write this string - * into all classes that reference it.
- * - *http://tinyurl.com/64t5n points to https://rome.dev.java.net - * Some servers ban user agents with "Java" in the name.
- * - */ - public static String DEFAULT_USER_AGENT = "Rome Client (http://tinyurl.com/64t5n)"; - - /** - * @param string The User-Agent to sent to servers - */ - public abstract void setUserAgent(String string); - - /** - * @return the User-Agent currently being sent to servers - */ - public abstract String getUserAgent(); - - /** - *Turn on or off rfc3229 delta encoding
- * - *See http://www.ietf.org/rfc/rfc3229.txt and http://bobwyman.pubsub.com/main/2004/09/using_rfc3229_w.html
- * - *NOTE: This is experimental and feedback is welcome!
- * - * @param useDeltaEncoding - */ - public abstract void setUsingDeltaEncoding(boolean useDeltaEncoding); - - /** - *Is this fetcher using rfc3229 delta encoding?
- * - * @return - */ - public abstract boolean isUsingDeltaEncoding(); - - /** - *Add a FetcherListener.
- * - *The FetcherListener will receive an FetcherEvent when - * a Fetcher event (feed polled, retrieved, etc) occurs
- * - * @param listener The FetcherListener to recieve the event - */ - public abstract void addFetcherEventListener(FetcherListener listener); - - /** - *Remove a FetcherListener
- * - * @param listener The FetcherListener to remove - */ - public abstract void removeFetcherEventListener(FetcherListener listener); - - /** - * Retrieve a feed over HTTP - * - * @param feedUrl A non-null URL of a RSS/Atom feed to retrieve - * @return A {@link com.sun.syndication.feed.synd.SyndFeed} object - * @throws IllegalArgumentException if the URL is null; - * @throws IOException if a TCP error occurs - * @throws FeedException if the feed is not valid - * @throws FetcherException if a HTTP error occurred - */ - public abstract SyndFeed retrieveFeed(URL feedUrl) - throws IllegalArgumentException, IOException, FeedException, FetcherException; - - public SyndFeed retrieveFeed(String userAgent, URL url) - throws IllegalArgumentException, IOException, FeedException, FetcherException; - - /** - * If set to true, the WireFeed will be made accessible from the SyndFeed object returned from the Fetcher - * via the originalWireFeed() method. Each Entry in the feed will have the corresponding wireEntry property set. - */ - void setPreserveWireFeed(boolean preserveWireFeed); -} diff --git a/src/java/com/sun/syndication/fetcher/FetcherEvent.java b/src/java/com/sun/syndication/fetcher/FetcherEvent.java deleted file mode 100644 index 8c705a3..0000000 --- a/src/java/com/sun/syndication/fetcher/FetcherEvent.java +++ /dev/null @@ -1,85 +0,0 @@ -package com.sun.syndication.fetcher; - -import java.util.EventObject; - -import com.sun.syndication.feed.synd.SyndFeed; - -/** - * Implementation note: FetcherEvent is not thread safe. Make sure that - * they are only ever accessed by one thread. If necessary, make all getters - * and setters synchronized, or alternatively make all fields final. - * - * @author nl - */ -public class FetcherEvent extends EventObject { - - private static final long serialVersionUID = 3985600601904140103L; - - public static final String EVENT_TYPE_FEED_POLLED = "FEED_POLLED"; - public static final String EVENT_TYPE_FEED_RETRIEVED = "FEED_RETRIEVED"; - public static final String EVENT_TYPE_FEED_UNCHANGED = "FEED_UNCHANGED"; - - private String eventType; - private String urlString; - private SyndFeed feed; - - public FetcherEvent(Object source) { - super(source); - } - - - public FetcherEvent(Object source, String urlStr, String eventType) { - this(source); - setUrlString(urlStr); - setEventType(eventType); - } - - public FetcherEvent(Object source, String urlStr, String eventType, SyndFeed feed) { - this(source, urlStr, eventType); - setFeed(feed); - } - - - /** - * @return Returns the feed. - * - *The feed will only be set if the eventType is EVENT_TYPE_FEED_RETRIEVED
- */ - public SyndFeed getFeed() { - return feed; - } - - /** - * @param feed The feed to set. - * - *The feed will only be set if the eventType is EVENT_TYPE_FEED_RETRIEVED
- */ - public void setFeed(SyndFeed feed) { - this.feed = feed; - } - - /** - * @return Returns the eventType. - */ - public String getEventType() { - return eventType; - } - /** - * @param eventType The eventType to set. - */ - public void setEventType(String eventType) { - this.eventType = eventType; - } - /** - * @return Returns the urlString. - */ - public String getUrlString() { - return urlString; - } - /** - * @param urlString The urlString to set. - */ - public void setUrlString(String urlString) { - this.urlString = urlString; - } -} diff --git a/src/java/com/sun/syndication/fetcher/FetcherException.java b/src/java/com/sun/syndication/fetcher/FetcherException.java deleted file mode 100644 index c263bfb..0000000 --- a/src/java/com/sun/syndication/fetcher/FetcherException.java +++ /dev/null @@ -1,51 +0,0 @@ -/* - * Copyright 2004 Sun Microsystems, Inc. - * - * Licensed 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 com.sun.syndication.fetcher; - -/** - * @author Nick Lothian - * - */ -public class FetcherException extends Exception { - private static final long serialVersionUID = -7479645796948092380L; - - int responseCode; - - public FetcherException(Throwable cause) { - super(); - initCause(cause); - } - - public FetcherException(String message, Throwable cause) { - super(message); - initCause(cause); - } - - public FetcherException(String message) { - super(message); - } - - public FetcherException(int responseCode, String message) { - this(message); - this.responseCode = responseCode; - } - - public int getResponseCode() { - return responseCode; - } - -} diff --git a/src/java/com/sun/syndication/fetcher/FetcherListener.java b/src/java/com/sun/syndication/fetcher/FetcherListener.java deleted file mode 100644 index 25bfda1..0000000 --- a/src/java/com/sun/syndication/fetcher/FetcherListener.java +++ /dev/null @@ -1,15 +0,0 @@ -package com.sun.syndication.fetcher; - -import java.util.EventListener; - - -public interface FetcherListener extends EventListener { - - /** - *Called when a fetcher event occurs
- * - * @param event the event that fired - */ - public void fetcherEvent(FetcherEvent event); - -} diff --git a/src/java/com/sun/syndication/fetcher/impl/AbstractFeedFetcher.java b/src/java/com/sun/syndication/fetcher/impl/AbstractFeedFetcher.java deleted file mode 100644 index 12d843f..0000000 --- a/src/java/com/sun/syndication/fetcher/impl/AbstractFeedFetcher.java +++ /dev/null @@ -1,219 +0,0 @@ -/* - * Copyright 2004 Sun Microsystems, Inc. - * - * Licensed 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 com.sun.syndication.fetcher.impl; - -import java.io.IOException; -import java.io.InputStream; -import java.net.URLConnection; -import java.util.Collections; -import java.util.HashSet; -import java.util.Iterator; -import java.util.Properties; -import java.util.Set; - -import com.sun.syndication.feed.synd.SyndFeed; -import com.sun.syndication.fetcher.FeedFetcher; -import com.sun.syndication.fetcher.FetcherEvent; -import com.sun.syndication.fetcher.FetcherException; -import com.sun.syndication.fetcher.FetcherListener; - - -public abstract class AbstractFeedFetcher implements FeedFetcher { - private final Set fetcherEventListeners; - private String userAgent; - private boolean usingDeltaEncoding; - private boolean preserveWireFeed; - - - - public AbstractFeedFetcher() { - fetcherEventListeners = Collections.synchronizedSet(new HashSet()); - - Properties props = new Properties(System.getProperties()); - String resourceName = "fetcher.properties"; - - try { - InputStream inputStream = this.getClass().getClassLoader().getResourceAsStream(resourceName); - if (inputStream == null) { - inputStream = Thread.currentThread().getContextClassLoader().getResourceAsStream(resourceName); - } - if (inputStream != null) { - props.load(inputStream); - System.getProperties().putAll(props); - inputStream.close(); - } else { - System.err.println("Could not find " + resourceName + " on classpath"); - } - } catch (IOException e) { - // do nothing - we don't want to fail just because we could not find the version - System.err.println("Error reading " + resourceName + " from classpath: " + e.getMessage()); - } - - - setUserAgent(DEFAULT_USER_AGENT + " Ver: " + System.getProperty("rome.fetcher.version", "UNKNOWN")); - } - - /** - * @return the User-Agent currently being sent to servers - */ - public synchronized String getUserAgent() { - return userAgent; - } - - /** - * @param string The User-Agent to sent to servers - */ - public synchronized void setUserAgent(String string) { - userAgent = string; - } - - /** - * @param eventType The event type to fire - * @param connection the current connection - */ - protected void fireEvent(String eventType, URLConnection connection) { - fireEvent(eventType, connection.getURL().toExternalForm(), null); - } - - - /** - * @param eventType The event type to fire - * @param connection the current connection - * @param feed The feed to pass to the event - */ - protected void fireEvent(String eventType, URLConnection connection, SyndFeed feed) { - fireEvent(eventType, connection.getURL().toExternalForm(), feed); - } - - /** - * @param eventType The event type to fire - * @param urlStr the current url as a string - */ - protected void fireEvent(String eventType, String urlStr) { - fireEvent(eventType, urlStr, null); - } - - /** - * @param eventType The event type to fire - * @param urlStr the current url as a string - * @param feed The feed to pass to the event - */ - protected void fireEvent(String eventType, String urlStr, SyndFeed feed) { - FetcherEvent fetcherEvent = new FetcherEvent(this, urlStr, eventType, feed); - synchronized(fetcherEventListeners) { - Iterator iter = fetcherEventListeners.iterator(); - while ( iter.hasNext()) { - FetcherListener fetcherEventListener = (FetcherListener) iter.next(); - fetcherEventListener.fetcherEvent(fetcherEvent); - } - } - } - - /** - * @see com.sun.syndication.fetcher.FeedFetcher#addFetcherEventListener(com.sun.syndication.fetcher.FetcherListener) - */ - public void addFetcherEventListener(FetcherListener listener) { - if (listener != null) { - fetcherEventListeners.add(listener); - } - - } - - /** - * @see com.sun.syndication.fetcher.FeedFetcher#removeFetcherEventListener(com.sun.syndication.fetcher.FetcherListener) - */ - public void removeFetcherEventListener(FetcherListener listener) { - if (listener != null) { - fetcherEventListeners.remove(listener); - } - } - - /** - * @return Returns the useDeltaEncoding. - */ - public synchronized boolean isUsingDeltaEncoding() { - return usingDeltaEncoding; - } - /** - * @param useDeltaEncoding The useDeltaEncoding to set. - */ - public synchronized void setUsingDeltaEncoding(boolean useDeltaEncoding) { - this.usingDeltaEncoding = useDeltaEncoding; - } - - /** - *Handles HTTP error codes.
- * - * @param responseCode the HTTP response code - * @throws FetcherException if response code is in the range 400 to 599 inclusive - */ - protected void handleErrorCodes(int responseCode) throws FetcherException { - // Handle 2xx codes as OK, so ignore them here - // 3xx codes are handled by the HttpURLConnection class - if (responseCode == 403) { - // Authentication is required - throwAuthenticationError(responseCode); - } else if (responseCode >= 400 && responseCode < 500) { - throw4XXError(responseCode); - } else if (responseCode >= 500 && responseCode < 600) { - throw new FetcherException(responseCode, "The server encounted an error. HTTP Response code was:" + responseCode); - } - } - - protected void throw4XXError(int responseCode) throws FetcherException { - throw new FetcherException(responseCode, "The requested resource could not be found. HTTP Response code was:" + responseCode); - } - - protected void throwAuthenticationError(int responseCode) throws FetcherException { - throw new FetcherException(responseCode, "Authentication required for that resource. HTTP Response code was:" + responseCode); - } - - /** - *Combine the entries in two feeds into a single feed.
- * - *The returned feed will have the same data as the newFeed parameter, with - * the entries from originalFeed appended to the end of its entries.
- * - * @param originalFeed - * @param newFeed - * @return - */ - public static SyndFeed combineFeeds(SyndFeed originalFeed, SyndFeed newFeed) { - SyndFeed result; - try { - result = (SyndFeed) newFeed.clone(); - - result.getEntries().addAll(result.getEntries().size(), originalFeed.getEntries()); - - return result; - } catch (CloneNotSupportedException e) { - IllegalArgumentException iae = new IllegalArgumentException("Cannot clone feed"); - iae.initCause(e); - throw iae; - } - } - - public boolean isPreserveWireFeed() { - return preserveWireFeed; - } - - public void setPreserveWireFeed(boolean preserveWireFeed) { - this.preserveWireFeed = preserveWireFeed; - } - -} diff --git a/src/java/com/sun/syndication/fetcher/impl/AbstractFeedFetcherBeanInfo.java b/src/java/com/sun/syndication/fetcher/impl/AbstractFeedFetcherBeanInfo.java deleted file mode 100644 index d8303a5..0000000 --- a/src/java/com/sun/syndication/fetcher/impl/AbstractFeedFetcherBeanInfo.java +++ /dev/null @@ -1,29 +0,0 @@ -package com.sun.syndication.fetcher.impl; - -import java.beans.EventSetDescriptor; -import java.beans.SimpleBeanInfo; -import java.lang.reflect.Method; - -import com.sun.syndication.fetcher.FetcherEvent; -import com.sun.syndication.fetcher.FetcherListener; - -public class AbstractFeedFetcherBeanInfo extends SimpleBeanInfo { - - public EventSetDescriptor[] getEventSetDescriptors() { - try { - Class clz = AbstractFeedFetcher.class; // get the class object which we'll describe - Method addMethod = clz.getMethod("addFetcherEventListener", new Class[] { FetcherListener.class }); - Method removeMethod = clz.getMethod("removeFetcherEventListener", new Class[] { FetcherListener.class }); - Method listenerMethod = FetcherListener.class.getMethod("fetcherEvent", new Class[] { FetcherEvent.class }); - - EventSetDescriptor est = new EventSetDescriptor("fetcherEvent", clz, new Method[] { listenerMethod }, addMethod, removeMethod); - EventSetDescriptor[] results = new EventSetDescriptor[] { est }; - - return results; - } catch (Exception e) { - // IntrospectionException, SecurityException and/or NoSuchMethodException can be thrown here - // the best we can do is to convert them to runtime exceptions - throw new RuntimeException(e); - } - } -} diff --git a/src/java/com/sun/syndication/fetcher/impl/DiskFeedInfoCache.java b/src/java/com/sun/syndication/fetcher/impl/DiskFeedInfoCache.java deleted file mode 100644 index 93c0d2b..0000000 --- a/src/java/com/sun/syndication/fetcher/impl/DiskFeedInfoCache.java +++ /dev/null @@ -1,134 +0,0 @@ -/* - * Copyright 2005 Sun Microsystems, Inc. - * - * Licensed 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 com.sun.syndication.fetcher.impl; - -import java.io.File; -import java.io.FileInputStream; -import java.io.FileNotFoundException; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.ObjectInputStream; -import java.io.ObjectOutputStream; -import java.net.URL; -import javax.swing.text.Utilities; - - -/** - * Disk based cache. - */ -public class DiskFeedInfoCache implements FeedFetcherCache { - - protected String cachePath = null; - public DiskFeedInfoCache(String cachePath) { - this.cachePath = cachePath; - } - public SyndFeedInfo getFeedInfo(URL url) { - SyndFeedInfo info = null; - String fileName = cachePath + File.separator + "feed_" - + replaceNonAlphanumeric(url.toString(),'_').trim(); - FileInputStream fis; - try { - fis = new FileInputStream(fileName); - ObjectInputStream ois = new ObjectInputStream(fis); - info = (SyndFeedInfo)ois.readObject(); - fis.close(); - } catch (FileNotFoundException fnfe) { - // That's OK, we'l return null - } catch (ClassNotFoundException cnfe) { - // Error writing to cache is fatal - throw new RuntimeException("Attempting to read from cache", cnfe); - } catch (IOException fnfe) { - // Error writing to cache is fatal - throw new RuntimeException("Attempting to read from cache", fnfe); - } - return info; - } - - public void setFeedInfo(URL url, SyndFeedInfo feedInfo) { - String fileName = cachePath + File.separator + "feed_" - + replaceNonAlphanumeric(url.toString(),'_').trim(); - FileOutputStream fos; - try { - fos = new FileOutputStream(fileName); - ObjectOutputStream oos = new ObjectOutputStream(fos); - oos.writeObject(feedInfo); - fos.flush(); - fos.close(); - } catch (Exception e) { - // Error writing to cache is fatal - throw new RuntimeException("Attempting to write to cache", e); - } - } - - public static String replaceNonAlphanumeric(String str, char subst) { - StringBuffer ret = new StringBuffer(str.length()); - char[] testChars = str.toCharArray(); - for (int i = 0; i < testChars.length; i++) { - if (Character.isLetterOrDigit(testChars[i])) { - ret.append(testChars[i]); - } else { - ret.append( subst ); - } - } - return ret.toString(); - } - - /** - * Clear the cache. - */ - public synchronized void clear() { - final File file = new File(this.cachePath); - //only do the delete if the directory exists - if( file.exists() && file.canWrite() ) { - //make the directory empty - final String[] files = file.list(); - final int len = files.length; - for( int i=0; iA very simple implementation of the {@link com.sun.syndication.fetcher.impl.FeedFetcherCache} interface.
- * - *This implementation uses a HashMap to cache retrieved feeds. This implementation is - * most suitible for sort term (client aggregator?) use, as the memory usage will increase - * over time as the number of feeds in the cache increases.
- * - * @author Nick Lothian - * - */ -public class HashMapFeedInfoCache implements FeedFetcherCache, Serializable { - private static final long serialVersionUID = -1594665619950916222L; - - static HashMapFeedInfoCache _instance; - - private Map infoCache; - - /** - *Constructor for HashMapFeedInfoCache
- * - *Only use this if you want multiple instances of the cache. - * Usually getInstance() is more appropriate.
- * - */ - public HashMapFeedInfoCache() { - setInfoCache(createInfoCache()); - } - - /** - * Get the global instance of the cache - * @return an implementation of FeedFetcherCache - */ - public static synchronized FeedFetcherCache getInstance() { - if (_instance == null) { - _instance = new HashMapFeedInfoCache(); - } - return _instance; - } - - protected Map createInfoCache() { - return (Collections.synchronizedMap(new HashMap())); - } - - - protected Object get(Object key) { - return getInfoCache().get(key); - } - - /** - * @see extensions.io.FeedFetcherCache#getFeedInfo(java.net.URL) - */ - public SyndFeedInfo getFeedInfo(URL feedUrl) { - return (SyndFeedInfo) get(feedUrl.toString()); - } - - protected void put(Object key, Object value) { - getInfoCache().put(key, value); - } - - /** - * @see extensions.io.FeedFetcherCache#setFeedInfo(java.net.URL, extensions.io.SyndFeedInfo) - */ - public void setFeedInfo(URL feedUrl, SyndFeedInfo syndFeedInfo) { - put(feedUrl.toString(), syndFeedInfo); - } - - protected synchronized final Map getInfoCache() { - return infoCache; - } - - /** - * The API of this class indicates that map must thread safe. In other - * words, be sure to wrap it in a synchronized map unless you know - * what you are doing. - * - * @param map the map to use as the info cache. - */ - protected synchronized final void setInfoCache(Map map) { - infoCache = map; - } - - /** - * @see com.sun.syndication.fetcher.impl.FeedFetcherCache#clear() - */ - public void clear() { - synchronized( infoCache ) { - infoCache.clear(); - } - } - - /** - * @see com.sun.syndication.fetcher.impl.FeedFetcherCache#remove(java.net.URL) - */ - public SyndFeedInfo remove( final URL url ) { - if( url == null ) return null; - - return (SyndFeedInfo) infoCache.remove( url.toString() ); - } - -} diff --git a/src/java/com/sun/syndication/fetcher/impl/HttpClientFeedFetcher.java b/src/java/com/sun/syndication/fetcher/impl/HttpClientFeedFetcher.java deleted file mode 100644 index effc6bb..0000000 --- a/src/java/com/sun/syndication/fetcher/impl/HttpClientFeedFetcher.java +++ /dev/null @@ -1,379 +0,0 @@ -/* - * Copyright 2004 Sun Microsystems, Inc. - * - * Licensed 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 com.sun.syndication.fetcher.impl; - -import com.sun.syndication.feed.synd.SyndFeed; -import com.sun.syndication.fetcher.FetcherEvent; -import com.sun.syndication.fetcher.FetcherException; -import com.sun.syndication.io.FeedException; -import com.sun.syndication.io.SyndFeedInput; -import com.sun.syndication.io.XmlReader; - -import org.apache.commons.httpclient.Credentials; -import org.apache.commons.httpclient.Header; -import org.apache.commons.httpclient.HttpClient; -import org.apache.commons.httpclient.HttpException; -import org.apache.commons.httpclient.HttpMethod; -import org.apache.commons.httpclient.methods.GetMethod; -import org.apache.commons.httpclient.params.HttpClientParams; - -import java.io.IOException; -import java.io.InputStream; - -import java.net.HttpURLConnection; -import java.net.MalformedURLException; -import java.net.URL; - -import java.util.zip.GZIPInputStream; - - -/** - * @author Nick Lothian - */ -public class HttpClientFeedFetcher extends AbstractFeedFetcher { - private CredentialSupplier credentialSupplier; - private FeedFetcherCache feedInfoCache; - private volatile HttpClientMethodCallbackIntf httpClientMethodCallback; - private volatile HttpClientParams httpClientParams; - - public HttpClientFeedFetcher() { - super(); - setHttpClientParams(new HttpClientParams()); - } - - /** - * @param cache - */ - public HttpClientFeedFetcher(FeedFetcherCache cache) { - this(); - setFeedInfoCache(cache); - } - - public HttpClientFeedFetcher(FeedFetcherCache cache, CredentialSupplier credentialSupplier) { - this(cache); - setCredentialSupplier(credentialSupplier); - } - - /** - * @param timeout Sets the connect timeout for the HttpClient but using the URLConnection method name. - * Uses the HttpClientParams method setConnectionManagerTimeout instead of setConnectTimeout - * - */ - public synchronized void setConnectTimeout(int timeout) { - httpClientParams.setConnectionManagerTimeout(timeout); - } - - /** - * @return The currently used connect timeout for the HttpClient but using the URLConnection method name. - * Uses the HttpClientParams method getConnectionManagerTimeout instead of getConnectTimeout - * - */ - public int getConnectTimeout() { - return (int) this.getHttpClientParams() - .getConnectionManagerTimeout(); - } - - /** - * @param credentialSupplier The credentialSupplier to set. - */ - public synchronized void setCredentialSupplier(CredentialSupplier credentialSupplier) { - this.credentialSupplier = credentialSupplier; - } - - /** - * @return Returns the credentialSupplier. - */ - public synchronized CredentialSupplier getCredentialSupplier() { - return credentialSupplier; - } - - /** - * @param feedInfoCache the feedInfoCache to set - */ - public synchronized void setFeedInfoCache(FeedFetcherCache feedInfoCache) { - this.feedInfoCache = feedInfoCache; - } - - /** - * @return the feedInfoCache. - */ - public synchronized FeedFetcherCache getFeedInfoCache() { - return feedInfoCache; - } - - public synchronized void setHttpClientMethodCallback(HttpClientMethodCallbackIntf httpClientMethodCallback) { - this.httpClientMethodCallback = httpClientMethodCallback; - } - - public HttpClientMethodCallbackIntf getHttpClientMethodCallback() { - return httpClientMethodCallback; - } - - /** - * @param httpClientParams The httpClientParams to set. - */ - public synchronized void setHttpClientParams(HttpClientParams httpClientParams) { - this.httpClientParams = httpClientParams; - } - - /** - * @return Returns the httpClientParams. - */ - public synchronized HttpClientParams getHttpClientParams() { - return this.httpClientParams; - } - - /** - * @return The currently used read timeout for the URLConnection, 0 is unlimited, i.e. no timeout - */ - public synchronized void setReadTimeout(int timeout) { - httpClientParams.setSoTimeout(timeout); - } - - /** - * @param timeout Sets the read timeout for the URLConnection to a specified timeout, in milliseconds. - */ - public int getReadTimeout() { - return (int) this.getHttpClientParams() - .getSoTimeout(); - } - - public SyndFeed retrieveFeed(URL url) throws IllegalArgumentException, IOException, FeedException, FetcherException { - return this.retrieveFeed(this.getUserAgent(), url); - } - - /** - * @see com.sun.syndication.fetcher.FeedFetcher#retrieveFeed(java.net.URL) - */ - public SyndFeed retrieveFeed(String userAgent, URL feedUrl) - throws IllegalArgumentException, IOException, FeedException, FetcherException { - if (feedUrl == null) { - throw new IllegalArgumentException("null is not a valid URL"); - } - - // TODO Fix this - //System.setProperty("org.apache.commons.logging.Log", "org.apache.commons.logging.impl.SimpleLog"); - HttpClient client = new HttpClient(httpClientParams); - - if (getCredentialSupplier() != null) { - client.getState() - .setAuthenticationPreemptive(true); - - // TODO what should realm be here? - Credentials credentials = getCredentialSupplier() - .getCredentials(null, feedUrl.getHost()); - - if (credentials != null) { - client.getState() - .setCredentials(null, feedUrl.getHost(), credentials); - } - } - - System.setProperty("httpclient.useragent", userAgent); - - String urlStr = feedUrl.toString(); - - HttpMethod method = new GetMethod(urlStr); - method.addRequestHeader("Accept-Encoding", "gzip"); - method.addRequestHeader("User-Agent", userAgent); - method.setFollowRedirects(true); - - if (httpClientMethodCallback != null) { - synchronized (httpClientMethodCallback) { - httpClientMethodCallback.afterHttpClientMethodCreate(method); - } - } - - FeedFetcherCache cache = getFeedInfoCache(); - - if (cache != null) { - // retrieve feed - try { - if (isUsingDeltaEncoding()) { - method.setRequestHeader("A-IM", "feed"); - } - - // get the feed info from the cache - // Note that syndFeedInfo will be null if it is not in the cache - SyndFeedInfo syndFeedInfo = cache.getFeedInfo(feedUrl); - - if (syndFeedInfo != null) { - method.setRequestHeader("If-None-Match", syndFeedInfo.getETag()); - - if (syndFeedInfo.getLastModified() instanceof String) { - method.setRequestHeader("If-Modified-Since", (String) syndFeedInfo.getLastModified()); - } - } - - int statusCode = client.executeMethod(method); - fireEvent(FetcherEvent.EVENT_TYPE_FEED_POLLED, urlStr); - handleErrorCodes(statusCode); - - SyndFeed feed = getFeed(syndFeedInfo, urlStr, method, statusCode); - - syndFeedInfo = buildSyndFeedInfo(feedUrl, urlStr, method, feed, statusCode); - - cache.setFeedInfo(new URL(urlStr), syndFeedInfo); - - // the feed may have been modified to pick up cached values - // (eg - for delta encoding) - feed = syndFeedInfo.getSyndFeed(); - - return feed; - } finally { - method.releaseConnection(); - method.recycle(); - } - } else { - // cache is not in use - try { - int statusCode = client.executeMethod(method); - fireEvent(FetcherEvent.EVENT_TYPE_FEED_POLLED, urlStr); - handleErrorCodes(statusCode); - - return getFeed(null, urlStr, method, statusCode); - } finally { - method.releaseConnection(); - method.recycle(); - } - } - } - - private SyndFeed getFeed(SyndFeedInfo syndFeedInfo, String urlStr, HttpMethod method, int statusCode) - throws IOException, HttpException, FetcherException, FeedException { - if ((statusCode == HttpURLConnection.HTTP_NOT_MODIFIED) && (syndFeedInfo != null)) { - fireEvent(FetcherEvent.EVENT_TYPE_FEED_UNCHANGED, urlStr); - - return syndFeedInfo.getSyndFeed(); - } - - SyndFeed feed = retrieveFeed(urlStr, method); - fireEvent(FetcherEvent.EVENT_TYPE_FEED_RETRIEVED, urlStr, feed); - - return feed; - } - - /** - * @param feedUrl - * @param urlStr - * @param method - * @param feed - * @return - * @throws MalformedURLException - */ - private SyndFeedInfo buildSyndFeedInfo(URL feedUrl, String urlStr, HttpMethod method, SyndFeed feed, int statusCode) - throws MalformedURLException { - SyndFeedInfo syndFeedInfo; - syndFeedInfo = new SyndFeedInfo(); - - // this may be different to feedURL because of 3XX redirects - syndFeedInfo.setUrl(new URL(urlStr)); - syndFeedInfo.setId(feedUrl.toString()); - - Header imHeader = method.getResponseHeader("IM"); - - if ((imHeader != null) && (imHeader.getValue() - .indexOf("feed") >= 0) && isUsingDeltaEncoding()) { - FeedFetcherCache cache = getFeedInfoCache(); - - if ((cache != null) && (statusCode == 226)) { - // client is setup to use http delta encoding and the server supports it and has returned a delta encoded response - // This response only includes new items - SyndFeedInfo cachedInfo = cache.getFeedInfo(feedUrl); - - if (cachedInfo != null) { - SyndFeed cachedFeed = cachedInfo.getSyndFeed(); - - // set the new feed to be the orginal feed plus the new items - feed = combineFeeds(cachedFeed, feed); - } - } - } - - Header lastModifiedHeader = method.getResponseHeader("Last-Modified"); - - if (lastModifiedHeader != null) { - syndFeedInfo.setLastModified(lastModifiedHeader.getValue()); - } - - Header eTagHeader = method.getResponseHeader("ETag"); - - if (eTagHeader != null) { - syndFeedInfo.setETag(eTagHeader.getValue()); - } - - syndFeedInfo.setSyndFeed(feed); - - return syndFeedInfo; - } - - /** - * @param client - * @param urlStr - * @param method - * @return - * @throws IOException - * @throws HttpException - * @throws FetcherException - * @throws FeedException - */ - private SyndFeed retrieveFeed(String urlStr, HttpMethod method) - throws IOException, HttpException, FetcherException, FeedException { - InputStream stream = null; - - if ((method.getResponseHeader("Content-Encoding") != null) && - ("gzip".equalsIgnoreCase(method.getResponseHeader("Content-Encoding").getValue()))) { - stream = new GZIPInputStream(method.getResponseBodyAsStream()); - } else { - stream = method.getResponseBodyAsStream(); - } - - try { - XmlReader reader = null; - - if (method.getResponseHeader("Content-Type") != null) { - reader = new XmlReader(stream, method.getResponseHeader("Content-Type").getValue(), true); - } else { - reader = new XmlReader(stream, true); - } - - SyndFeedInput syndFeedInput = new SyndFeedInput(); - syndFeedInput.setPreserveWireFeed(isPreserveWireFeed()); - - return syndFeedInput.build(reader); - } finally { - if (stream != null) { - stream.close(); - } - } - } - - public interface CredentialSupplier { - public Credentials getCredentials(String realm, String host); - } - - public interface HttpClientMethodCallbackIntf { - /** - * Allows access to the underlying HttpClient HttpMethod object. - * Note that in most cases, method.setRequestHeader(String, String) - * is what you want to do (rather than method.addRequestHeader(String, String)) - * - * @param method - */ - public void afterHttpClientMethodCreate(HttpMethod method); - } -} diff --git a/src/java/com/sun/syndication/fetcher/impl/HttpURLFeedFetcher.java b/src/java/com/sun/syndication/fetcher/impl/HttpURLFeedFetcher.java deleted file mode 100644 index 0f1f193..0000000 --- a/src/java/com/sun/syndication/fetcher/impl/HttpURLFeedFetcher.java +++ /dev/null @@ -1,299 +0,0 @@ -/* - * Copyright 2004 Sun Microsystems, Inc. - * - * Licensed 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 com.sun.syndication.fetcher.impl; - -import java.io.BufferedInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.net.HttpURLConnection; -import java.net.URL; -import java.net.URLConnection; -import java.util.zip.GZIPInputStream; - -import com.sun.syndication.feed.synd.SyndFeed; -import com.sun.syndication.fetcher.FetcherEvent; -import com.sun.syndication.fetcher.FetcherException; -import com.sun.syndication.io.FeedException; -import com.sun.syndication.io.SyndFeedInput; -import com.sun.syndication.io.XmlReader; - -/** - *Class to retrieve syndication files via HTTP.
- * - *If passed a {@link com.sun.syndication.fetcher.impl.FeedFetcherCache} in the - * constructor it will use conditional gets to only retrieve modified content.
- * - *The class uses the Accept-Encoding: gzip header to retrieve gzipped feeds where - * supported by the server.
- * - *Simple usage: - *
- * // create the cache - * FeedFetcherCache feedInfoCache = HashMapFeedInfoCache.getFeedInfoCache(); - * // retrieve the feed the first time - * // any subsequent request will use conditional gets and only - * // retrieve the resource if it has changed - * SyndFeed feed = new HttpURLFeedFetcher(feedInfoCache).retrieveFeed(feedUrl); - *- * - * - * - * @see http://fishbowl.pastiche.org/2002/10/21/http_conditional_get_for_rss_hackers - * @see http://diveintomark.org/archives/2003/07/21/atom_aggregator_behavior_http_level - * @see http://bobwyman.pubsub.com/main/2004/09/using_rfc3229_w.html - * @author Nick Lothian - */ -public class HttpURLFeedFetcher extends AbstractFeedFetcher { - static final int POLL_EVENT = 1; - static final int RETRIEVE_EVENT = 2; - static final int UNCHANGED_EVENT = 3; - - private FeedFetcherCache feedInfoCache; - - - /** - * Constructor to use HttpURLFeedFetcher without caching of feeds - * - */ - public HttpURLFeedFetcher() { - super(); - } - - /** - * Constructor to enable HttpURLFeedFetcher to cache feeds - * - * @param feedCache - an instance of the FeedFetcherCache interface - */ - public HttpURLFeedFetcher(FeedFetcherCache feedInfoCache) { - this(); - setFeedInfoCache(feedInfoCache); - } - - public SyndFeed retrieveFeed(URL feedUrl) throws IllegalArgumentException, IOException, FeedException, FetcherException { - return this.retrieveFeed(this.getUserAgent(), feedUrl); - } - - /** - * Retrieve a feed over HTTP - * - * @param feedUrl A non-null URL of a RSS/Atom feed to retrieve - * @return A {@link com.sun.syndication.feed.synd.SyndFeed} object - * @throws IllegalArgumentException if the URL is null; - * @throws IOException if a TCP error occurs - * @throws FeedException if the feed is not valid - * @throws FetcherException if a HTTP error occurred - */ - public SyndFeed retrieveFeed(String userAgent, URL feedUrl) throws IllegalArgumentException, IOException, FeedException, FetcherException { - if (feedUrl == null) { - throw new IllegalArgumentException("null is not a valid URL"); - } - - URLConnection connection = feedUrl.openConnection(); - if (!(connection instanceof HttpURLConnection)) { - throw new IllegalArgumentException(feedUrl.toExternalForm() + " is not a valid HTTP Url"); - } - HttpURLConnection httpConnection = (HttpURLConnection)connection; - // httpConnection.setInstanceFollowRedirects(true); // this is true by default, but can be changed on a claswide basis - - FeedFetcherCache cache = getFeedInfoCache(); - if (cache != null) { - SyndFeedInfo syndFeedInfo = cache.getFeedInfo(feedUrl); - setRequestHeaders(connection, syndFeedInfo); - httpConnection.connect(); - try { - fireEvent(FetcherEvent.EVENT_TYPE_FEED_POLLED, connection); - - if (syndFeedInfo == null) { - // this is a feed that hasn't been retrieved - syndFeedInfo = new SyndFeedInfo(); - retrieveAndCacheFeed(feedUrl, syndFeedInfo, httpConnection); - } else { - // check the response code - int responseCode = httpConnection.getResponseCode(); - if (responseCode != HttpURLConnection.HTTP_NOT_MODIFIED) { - // the response code is not 304 NOT MODIFIED - // This is either because the feed server - // does not support condition gets - // or because the feed hasn't changed - retrieveAndCacheFeed(feedUrl, syndFeedInfo, httpConnection); - } else { - // the feed does not need retrieving - fireEvent(FetcherEvent.EVENT_TYPE_FEED_UNCHANGED, connection); - } - } - - return syndFeedInfo.getSyndFeed(); - } finally { - httpConnection.disconnect(); - } - } else { - fireEvent(FetcherEvent.EVENT_TYPE_FEED_POLLED, connection); - InputStream inputStream = null; - setRequestHeaders(connection, null); - - connection.addRequestProperty("User-Agent", userAgent); - - httpConnection.connect(); - try { - inputStream = httpConnection.getInputStream(); - return getSyndFeedFromStream(inputStream, connection); - } catch (java.io.IOException e) { - handleErrorCodes(((HttpURLConnection)connection).getResponseCode()); - } finally { - if (inputStream != null) { - inputStream.close(); - } - httpConnection.disconnect(); - } - // we will never actually get to this line - return null; - } - } - - protected void retrieveAndCacheFeed(URL feedUrl, SyndFeedInfo syndFeedInfo, HttpURLConnection connection) throws IllegalArgumentException, FeedException, FetcherException, IOException { - handleErrorCodes(connection.getResponseCode()); - - resetFeedInfo(feedUrl, syndFeedInfo, connection); - FeedFetcherCache cache = getFeedInfoCache(); - // resetting feed info in the cache - // could be needed for some implementations - // of FeedFetcherCache (eg, distributed HashTables) - if (cache != null) { - cache.setFeedInfo(feedUrl, syndFeedInfo); - } - } - - protected void resetFeedInfo(URL orignalUrl, SyndFeedInfo syndFeedInfo, HttpURLConnection connection) throws IllegalArgumentException, IOException, FeedException { - // need to always set the URL because this may have changed due to 3xx redirects - syndFeedInfo.setUrl(connection.getURL()); - - // the ID is a persistant value that should stay the same even if the URL for the - // feed changes (eg, by 3xx redirects) - syndFeedInfo.setId(orignalUrl.toString()); - - // This will be 0 if the server doesn't support or isn't setting the last modified header - syndFeedInfo.setLastModified(new Long(connection.getLastModified())); - - // This will be null if the server doesn't support or isn't setting the ETag header - syndFeedInfo.setETag(connection.getHeaderField("ETag")); - - // get the contents - InputStream inputStream = null; - try { - inputStream = connection.getInputStream(); - SyndFeed syndFeed = getSyndFeedFromStream(inputStream, connection); - - String imHeader = connection.getHeaderField("IM"); - if (isUsingDeltaEncoding() && (imHeader!= null && imHeader.indexOf("feed") >= 0)) { - FeedFetcherCache cache = getFeedInfoCache(); - if (cache != null && connection.getResponseCode() == 226) { - // client is setup to use http delta encoding and the server supports it and has returned a delta encoded response - // This response only includes new items - SyndFeedInfo cachedInfo = cache.getFeedInfo(orignalUrl); - if (cachedInfo != null) { - SyndFeed cachedFeed = cachedInfo.getSyndFeed(); - - // set the new feed to be the orginal feed plus the new items - syndFeed = combineFeeds(cachedFeed, syndFeed); - } - } - } - - syndFeedInfo.setSyndFeed(syndFeed); - } finally { - if (inputStream != null) { - inputStream.close(); - } - } - } - - /** - *
Set appropriate HTTP headers, including conditional get and gzip encoding headers
- * - * @param connection A URLConnection - * @param syndFeedInfo The SyndFeedInfo for the feed to be retrieved. May be null - */ - protected void setRequestHeaders(URLConnection connection, SyndFeedInfo syndFeedInfo) { - if (syndFeedInfo != null) { - // set the headers to get feed only if modified - // we support the use of both last modified and eTag headers - if (syndFeedInfo.getLastModified() != null) { - Object lastModified = syndFeedInfo.getLastModified(); - if (lastModified instanceof Long) { - connection.setIfModifiedSince(((Long)syndFeedInfo.getLastModified()).longValue()); - } - } - if (syndFeedInfo.getETag() != null) { - connection.setRequestProperty("If-None-Match", syndFeedInfo.getETag()); - } - - } - // header to retrieve feed gzipped - connection.setRequestProperty("Accept-Encoding", "gzip"); - - if (isUsingDeltaEncoding()) { - connection.addRequestProperty("A-IM", "feed"); - } - } - - private SyndFeed readSyndFeedFromStream(InputStream inputStream, URLConnection connection) throws IOException, IllegalArgumentException, FeedException { - BufferedInputStream is; - if ("gzip".equalsIgnoreCase(connection.getContentEncoding())) { - // handle gzip encoded content - is = new BufferedInputStream(new GZIPInputStream(inputStream)); - } else { - is = new BufferedInputStream(inputStream); - } - - //InputStreamReader reader = new InputStreamReader(is, ResponseHandler.getCharacterEncoding(connection)); - - //SyndFeedInput input = new SyndFeedInput(); - - XmlReader reader = null; - if (connection.getHeaderField("Content-Type") != null) { - reader = new XmlReader(is, connection.getHeaderField("Content-Type"), true); - } else { - reader = new XmlReader(is, true); - } - - SyndFeedInput syndFeedInput = new SyndFeedInput(); - syndFeedInput.setPreserveWireFeed(isPreserveWireFeed()); - - return syndFeedInput.build(reader); - - } - - private SyndFeed getSyndFeedFromStream(InputStream inputStream, URLConnection connection) throws IOException, IllegalArgumentException, FeedException { - SyndFeed feed = readSyndFeedFromStream(inputStream, connection); - fireEvent(FetcherEvent.EVENT_TYPE_FEED_RETRIEVED, connection, feed); - return feed; - } - - /** - * @return The FeedFetcherCache used by this fetcher (Could be null) - */ - public synchronized FeedFetcherCache getFeedInfoCache() { - return feedInfoCache; - } - - /** - * @param cache The cache to be used by this fetcher (pass null to stop using a cache) - */ - public synchronized void setFeedInfoCache(FeedFetcherCache cache) { - feedInfoCache = cache; - } -} diff --git a/src/java/com/sun/syndication/fetcher/impl/LinkedHashMapFeedInfoCache.java b/src/java/com/sun/syndication/fetcher/impl/LinkedHashMapFeedInfoCache.java deleted file mode 100644 index 736f10d..0000000 --- a/src/java/com/sun/syndication/fetcher/impl/LinkedHashMapFeedInfoCache.java +++ /dev/null @@ -1,70 +0,0 @@ -package com.sun.syndication.fetcher.impl; - -import java.util.Collections; -import java.util.LinkedHashMap; -import java.util.Map; - -/** - *An implementation of the {@link com.sun.syndication.fetcher.impl.FeedFetcherCache} interface.
- * - *Unlike the HashMapFeedInfoCache this implementation will not grow unbound
- * - * @author Javier Kohen - * @author Nick Lothian - * - */ -public class LinkedHashMapFeedInfoCache extends HashMapFeedInfoCache { - private final class CacheImpl extends LinkedHashMap { - private static final long serialVersionUID = -6977191330127794920L; - - public CacheImpl() { - super(16, 0.75F, true); - } - - protected boolean removeEldestEntry(Map.Entry eldest) { - return size() > getMaxEntries(); - } - } - - private static final int DEFAULT_MAX_ENTRIES = 20; - - private static final long serialVersionUID = 1694228973357997417L; - - private int maxEntries = DEFAULT_MAX_ENTRIES; - - private final static LinkedHashMapFeedInfoCache _instance = new LinkedHashMapFeedInfoCache(); - - - /** - * Get the global instance of the cache - * @return an implementation of FeedFetcherCache - */ - public static final FeedFetcherCache getInstance() { - return _instance; - } - - /** - *Constructor for HashMapFeedInfoCache
- * - *Only use this if you want multiple instances of the cache. - * Usually {@link #getInstance()} is more appropriate.
- * - * @see #getInstance() - */ - public LinkedHashMapFeedInfoCache() { - super(); - } - - protected Map createInfoCache() { - return Collections.synchronizedMap(new CacheImpl()); - } - - public synchronized final int getMaxEntries() { - return maxEntries; - } - - public synchronized final void setMaxEntries(int maxEntries) { - this.maxEntries = maxEntries; - } - -} diff --git a/src/java/com/sun/syndication/fetcher/impl/ResponseHandler.java b/src/java/com/sun/syndication/fetcher/impl/ResponseHandler.java deleted file mode 100644 index db877fe..0000000 --- a/src/java/com/sun/syndication/fetcher/impl/ResponseHandler.java +++ /dev/null @@ -1,57 +0,0 @@ -/* - * Copyright 2004 Sun Microsystems, Inc. - * - * Licensed 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 com.sun.syndication.fetcher.impl; - -import java.net.URLConnection; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -/** - * Utility class to help deal with HTTP responses - * - */ -public class ResponseHandler { - public static final String defaultCharacterEncoding = "ISO-8859-1"; - - private final static Pattern characterEncodingPattern = Pattern.compile("charset=([.[^; ]]*)"); - - public static String getCharacterEncoding(URLConnection connection) { - return getCharacterEncoding(connection.getContentType()); - } - - /** - * - *Gets the character encoding of a response. (Note that this is different to - * the content-encoding)
- * - * @param contentTypeHeader the value of the content-type HTTP header eg: text/html; charset=ISO-8859-4 - * @return the character encoding, eg: ISO-8859-4 - */ - public static String getCharacterEncoding(String contentTypeHeader) { - if (contentTypeHeader == null) { - return defaultCharacterEncoding; - } - - Matcher m = characterEncodingPattern.matcher(contentTypeHeader); - //if (!m.matches()) { - if (!m.find()) { - return defaultCharacterEncoding; - } else { - return m.group(1); - } - } -} diff --git a/src/java/com/sun/syndication/fetcher/impl/SyndFeedInfo.java b/src/java/com/sun/syndication/fetcher/impl/SyndFeedInfo.java deleted file mode 100644 index a19f5fe..0000000 --- a/src/java/com/sun/syndication/fetcher/impl/SyndFeedInfo.java +++ /dev/null @@ -1,149 +0,0 @@ -/* - * Copyright 2004 Sun Microsystems, Inc. - * - * Licensed 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 com.sun.syndication.fetcher.impl; - -import java.io.Serializable; -import java.net.URL; - -import com.sun.syndication.feed.impl.ObjectBean; -import com.sun.syndication.feed.synd.SyndFeed; - -/** - *A class to represent a {@link com.sun.syndication.feed.synd.SyndFeed} - * and some useful information about it.
- * - *This class is thread safe, as expected by the different feed fetcher - * implementations.
- * - * @author Nick Lothian - */ -public class SyndFeedInfo implements Cloneable, Serializable { - private static final long serialVersionUID = -1874786860901426015L; - - private final ObjectBean _objBean; - private String id; - private URL url; - private Object lastModified; - private String eTag; - private SyndFeed syndFeed; - - public SyndFeedInfo() { - _objBean = new ObjectBean(this.getClass(),this); - } - - /** - * Creates a deep 'bean' clone of the object. - *- * @return a clone of the object. - * @throws CloneNotSupportedException thrown if an element of the object cannot be cloned. - * - */ - public Object clone() throws CloneNotSupportedException { - return _objBean.clone(); - } - - /** - * Indicates whether some other object is "equal to" this one as defined by the Object equals() method. - *
- * @param other he reference object with which to compare. - * @return true if 'this' object is equal to the 'other' object. - * - */ - public boolean equals(Object other) { - return _objBean.equals(other); - } - - /** - * Returns a hashcode value for the object. - *
- * It follows the contract defined by the Object hashCode() method. - *
- * @return the hashcode of the bean object. - * - */ - public int hashCode() { - return _objBean.hashCode(); - } - - /** - * Returns the String representation for the object. - *
- * @return String representation for the object. - * - */ - public String toString() { - return _objBean.toString(); - } - - - /** - * @return the ETag the feed was last retrieved with - */ - public synchronized String getETag() { - return eTag; - } - - /** - * @return the last modified date for the feed - */ - public synchronized Object getLastModified() { - return lastModified; - } - - /** - * @return the URL the feed was served from - */ - public synchronized URL getUrl() { - return url; - } - - public synchronized void setETag(String string) { - eTag = string; - } - - public synchronized void setLastModified(Object o) { - lastModified = o; - } - - public synchronized void setUrl(URL url) { - this.url = url; - } - - public synchronized SyndFeed getSyndFeed() { - return syndFeed; - } - - public synchronized void setSyndFeed(SyndFeed feed) { - syndFeed = feed; - } - - /** - * @return A unique ID to identify the feed - */ - public synchronized String getId() { - return id; - } - - /** - * @param string A unique ID to identify the feed. Note that if the URL of the feed - * changes this will remain the same - */ - public synchronized void setId(String string) { - id = string; - } - -} diff --git a/src/java/com/sun/syndication/fetcher/samples/FeedAggregator.java b/src/java/com/sun/syndication/fetcher/samples/FeedAggregator.java deleted file mode 100644 index 05e160c..0000000 --- a/src/java/com/sun/syndication/fetcher/samples/FeedAggregator.java +++ /dev/null @@ -1,92 +0,0 @@ -/* - * Copyright 2004 Sun Microsystems, Inc. - * - * Licensed 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 com.sun.syndication.fetcher.samples; - -import java.io.PrintWriter; -import java.net.URL; -import java.util.ArrayList; -import java.util.List; - -import com.sun.syndication.feed.synd.SyndFeedImpl; -import com.sun.syndication.feed.synd.SyndFeed; -import com.sun.syndication.fetcher.FeedFetcher; -import com.sun.syndication.fetcher.impl.FeedFetcherCache; -import com.sun.syndication.fetcher.impl.HashMapFeedInfoCache; -import com.sun.syndication.fetcher.impl.HttpURLFeedFetcher; -import com.sun.syndication.io.SyndFeedOutput; - -/** - *
It aggregates a list of RSS/Atom feeds (they can be of different types) - * into a single feed of the specified type.
- * - *Converted from the original FeedAggregator sample
- * - * @author Alejandro Abdelnur - * @author Nick Lothian - * - */ -public class FeedAggregator { - - public static void main(String[] args) { - boolean ok = false; - if (args.length>=2) { - try { - String outputType = args[0]; - - SyndFeed feed = new SyndFeedImpl(); - feed.setFeedType(outputType); - - feed.setTitle("Aggregated Feed"); - feed.setDescription("Anonymous Aggregated Feed"); - feed.setAuthor("anonymous"); - feed.setLink("http://www.anonymous.com"); - - List entries = new ArrayList(); - feed.setEntries(entries); - - FeedFetcherCache feedInfoCache = HashMapFeedInfoCache.getInstance(); - FeedFetcher feedFetcher = new HttpURLFeedFetcher(feedInfoCache); - - for (int i=1;iThe default user agent. It is not marked final so + * buggy java compiler will not write this string + * into all classes that reference it.
+ * + *http://tinyurl.com/64t5n points to https://rome.dev.java.net + * Some servers ban user agents with "Java" in the name.
+ * + */ + public static String DEFAULT_USER_AGENT = "Rome Client (http://tinyurl.com/64t5n)"; + + /** + * @param string The User-Agent to sent to servers + */ + public abstract void setUserAgent(String string); + + /** + * @return the User-Agent currently being sent to servers + */ + public abstract String getUserAgent(); + + /** + *Turn on or off rfc3229 delta encoding
+ * + *See http://www.ietf.org/rfc/rfc3229.txt and http://bobwyman.pubsub.com/main/2004/09/using_rfc3229_w.html
+ * + *NOTE: This is experimental and feedback is welcome!
+ * + * @param useDeltaEncoding + */ + public abstract void setUsingDeltaEncoding(boolean useDeltaEncoding); + + /** + *Is this fetcher using rfc3229 delta encoding?
+ * + * @return + */ + public abstract boolean isUsingDeltaEncoding(); + + /** + *Add a FetcherListener.
+ * + *The FetcherListener will receive an FetcherEvent when + * a Fetcher event (feed polled, retrieved, etc) occurs
+ * + * @param listener The FetcherListener to recieve the event + */ + public abstract void addFetcherEventListener(FetcherListener listener); + + /** + *Remove a FetcherListener
+ * + * @param listener The FetcherListener to remove + */ + public abstract void removeFetcherEventListener(FetcherListener listener); + + /** + * Retrieve a feed over HTTP + * + * @param feedUrl A non-null URL of a RSS/Atom feed to retrieve + * @return A {@link com.sun.syndication.feed.synd.SyndFeed} object + * @throws IllegalArgumentException if the URL is null; + * @throws IOException if a TCP error occurs + * @throws FeedException if the feed is not valid + * @throws FetcherException if a HTTP error occurred + */ + public abstract SyndFeed retrieveFeed(URL feedUrl) + throws IllegalArgumentException, IOException, FeedException, FetcherException; + + public SyndFeed retrieveFeed(String userAgent, URL url) + throws IllegalArgumentException, IOException, FeedException, FetcherException; + + /** + * If set to true, the WireFeed will be made accessible from the SyndFeed object returned from the Fetcher + * via the originalWireFeed() method. Each Entry in the feed will have the corresponding wireEntry property set. + */ + void setPreserveWireFeed(boolean preserveWireFeed); +} diff --git a/src/java/org/rometools/fetcher/FetcherEvent.java b/src/java/org/rometools/fetcher/FetcherEvent.java new file mode 100644 index 0000000..232af89 --- /dev/null +++ b/src/java/org/rometools/fetcher/FetcherEvent.java @@ -0,0 +1,85 @@ +package org.rometools.fetcher; + +import java.util.EventObject; + +import com.sun.syndication.feed.synd.SyndFeed; + +/** + * Implementation note: FetcherEvent is not thread safe. Make sure that + * they are only ever accessed by one thread. If necessary, make all getters + * and setters synchronized, or alternatively make all fields final. + * + * @author nl + */ +public class FetcherEvent extends EventObject { + + private static final long serialVersionUID = 3985600601904140103L; + + public static final String EVENT_TYPE_FEED_POLLED = "FEED_POLLED"; + public static final String EVENT_TYPE_FEED_RETRIEVED = "FEED_RETRIEVED"; + public static final String EVENT_TYPE_FEED_UNCHANGED = "FEED_UNCHANGED"; + + private String eventType; + private String urlString; + private SyndFeed feed; + + public FetcherEvent(Object source) { + super(source); + } + + + public FetcherEvent(Object source, String urlStr, String eventType) { + this(source); + setUrlString(urlStr); + setEventType(eventType); + } + + public FetcherEvent(Object source, String urlStr, String eventType, SyndFeed feed) { + this(source, urlStr, eventType); + setFeed(feed); + } + + + /** + * @return Returns the feed. + * + *The feed will only be set if the eventType is EVENT_TYPE_FEED_RETRIEVED
+ */ + public SyndFeed getFeed() { + return feed; + } + + /** + * @param feed The feed to set. + * + *The feed will only be set if the eventType is EVENT_TYPE_FEED_RETRIEVED
+ */ + public void setFeed(SyndFeed feed) { + this.feed = feed; + } + + /** + * @return Returns the eventType. + */ + public String getEventType() { + return eventType; + } + /** + * @param eventType The eventType to set. + */ + public void setEventType(String eventType) { + this.eventType = eventType; + } + /** + * @return Returns the urlString. + */ + public String getUrlString() { + return urlString; + } + /** + * @param urlString The urlString to set. + */ + public void setUrlString(String urlString) { + this.urlString = urlString; + } +} diff --git a/src/java/org/rometools/fetcher/FetcherException.java b/src/java/org/rometools/fetcher/FetcherException.java new file mode 100644 index 0000000..48575ec --- /dev/null +++ b/src/java/org/rometools/fetcher/FetcherException.java @@ -0,0 +1,51 @@ +/* + * Copyright 2004 Sun Microsystems, Inc. + * + * Licensed 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.rometools.fetcher; + +/** + * @author Nick Lothian + * + */ +public class FetcherException extends Exception { + private static final long serialVersionUID = -7479645796948092380L; + + int responseCode; + + public FetcherException(Throwable cause) { + super(); + initCause(cause); + } + + public FetcherException(String message, Throwable cause) { + super(message); + initCause(cause); + } + + public FetcherException(String message) { + super(message); + } + + public FetcherException(int responseCode, String message) { + this(message); + this.responseCode = responseCode; + } + + public int getResponseCode() { + return responseCode; + } + +} diff --git a/src/java/org/rometools/fetcher/FetcherListener.java b/src/java/org/rometools/fetcher/FetcherListener.java new file mode 100644 index 0000000..3424456 --- /dev/null +++ b/src/java/org/rometools/fetcher/FetcherListener.java @@ -0,0 +1,15 @@ +package org.rometools.fetcher; + +import java.util.EventListener; + + +public interface FetcherListener extends EventListener { + + /** + *Called when a fetcher event occurs
+ * + * @param event the event that fired + */ + public void fetcherEvent(FetcherEvent event); + +} diff --git a/src/java/org/rometools/fetcher/impl/AbstractFeedFetcher.java b/src/java/org/rometools/fetcher/impl/AbstractFeedFetcher.java new file mode 100644 index 0000000..4b1823f --- /dev/null +++ b/src/java/org/rometools/fetcher/impl/AbstractFeedFetcher.java @@ -0,0 +1,219 @@ +/* + * Copyright 2004 Sun Microsystems, Inc. + * + * Licensed 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.rometools.fetcher.impl; + +import java.io.IOException; +import java.io.InputStream; +import java.net.URLConnection; +import java.util.Collections; +import java.util.HashSet; +import java.util.Iterator; +import java.util.Properties; +import java.util.Set; + +import com.sun.syndication.feed.synd.SyndFeed; +import org.rometools.fetcher.FeedFetcher; +import org.rometools.fetcher.FetcherEvent; +import org.rometools.fetcher.FetcherException; +import org.rometools.fetcher.FetcherListener; + + +public abstract class AbstractFeedFetcher implements FeedFetcher { + private final Set fetcherEventListeners; + private String userAgent; + private boolean usingDeltaEncoding; + private boolean preserveWireFeed; + + + + public AbstractFeedFetcher() { + fetcherEventListeners = Collections.synchronizedSet(new HashSet()); + + Properties props = new Properties(System.getProperties()); + String resourceName = "fetcher.properties"; + + try { + InputStream inputStream = this.getClass().getClassLoader().getResourceAsStream(resourceName); + if (inputStream == null) { + inputStream = Thread.currentThread().getContextClassLoader().getResourceAsStream(resourceName); + } + if (inputStream != null) { + props.load(inputStream); + System.getProperties().putAll(props); + inputStream.close(); + } else { + System.err.println("Could not find " + resourceName + " on classpath"); + } + } catch (IOException e) { + // do nothing - we don't want to fail just because we could not find the version + System.err.println("Error reading " + resourceName + " from classpath: " + e.getMessage()); + } + + + setUserAgent(DEFAULT_USER_AGENT + " Ver: " + System.getProperty("rome.fetcher.version", "UNKNOWN")); + } + + /** + * @return the User-Agent currently being sent to servers + */ + public synchronized String getUserAgent() { + return userAgent; + } + + /** + * @param string The User-Agent to sent to servers + */ + public synchronized void setUserAgent(String string) { + userAgent = string; + } + + /** + * @param eventType The event type to fire + * @param connection the current connection + */ + protected void fireEvent(String eventType, URLConnection connection) { + fireEvent(eventType, connection.getURL().toExternalForm(), null); + } + + + /** + * @param eventType The event type to fire + * @param connection the current connection + * @param feed The feed to pass to the event + */ + protected void fireEvent(String eventType, URLConnection connection, SyndFeed feed) { + fireEvent(eventType, connection.getURL().toExternalForm(), feed); + } + + /** + * @param eventType The event type to fire + * @param urlStr the current url as a string + */ + protected void fireEvent(String eventType, String urlStr) { + fireEvent(eventType, urlStr, null); + } + + /** + * @param eventType The event type to fire + * @param urlStr the current url as a string + * @param feed The feed to pass to the event + */ + protected void fireEvent(String eventType, String urlStr, SyndFeed feed) { + FetcherEvent fetcherEvent = new FetcherEvent(this, urlStr, eventType, feed); + synchronized(fetcherEventListeners) { + Iterator iter = fetcherEventListeners.iterator(); + while ( iter.hasNext()) { + FetcherListener fetcherEventListener = (FetcherListener) iter.next(); + fetcherEventListener.fetcherEvent(fetcherEvent); + } + } + } + + /** + * @see com.sun.syndication.fetcher.FeedFetcher#addFetcherEventListener(com.sun.syndication.fetcher.FetcherListener) + */ + public void addFetcherEventListener(FetcherListener listener) { + if (listener != null) { + fetcherEventListeners.add(listener); + } + + } + + /** + * @see com.sun.syndication.fetcher.FeedFetcher#removeFetcherEventListener(com.sun.syndication.fetcher.FetcherListener) + */ + public void removeFetcherEventListener(FetcherListener listener) { + if (listener != null) { + fetcherEventListeners.remove(listener); + } + } + + /** + * @return Returns the useDeltaEncoding. + */ + public synchronized boolean isUsingDeltaEncoding() { + return usingDeltaEncoding; + } + /** + * @param useDeltaEncoding The useDeltaEncoding to set. + */ + public synchronized void setUsingDeltaEncoding(boolean useDeltaEncoding) { + this.usingDeltaEncoding = useDeltaEncoding; + } + + /** + *Handles HTTP error codes.
+ * + * @param responseCode the HTTP response code + * @throws FetcherException if response code is in the range 400 to 599 inclusive + */ + protected void handleErrorCodes(int responseCode) throws FetcherException { + // Handle 2xx codes as OK, so ignore them here + // 3xx codes are handled by the HttpURLConnection class + if (responseCode == 403) { + // Authentication is required + throwAuthenticationError(responseCode); + } else if (responseCode >= 400 && responseCode < 500) { + throw4XXError(responseCode); + } else if (responseCode >= 500 && responseCode < 600) { + throw new FetcherException(responseCode, "The server encounted an error. HTTP Response code was:" + responseCode); + } + } + + protected void throw4XXError(int responseCode) throws FetcherException { + throw new FetcherException(responseCode, "The requested resource could not be found. HTTP Response code was:" + responseCode); + } + + protected void throwAuthenticationError(int responseCode) throws FetcherException { + throw new FetcherException(responseCode, "Authentication required for that resource. HTTP Response code was:" + responseCode); + } + + /** + *Combine the entries in two feeds into a single feed.
+ * + *The returned feed will have the same data as the newFeed parameter, with + * the entries from originalFeed appended to the end of its entries.
+ * + * @param originalFeed + * @param newFeed + * @return + */ + public static SyndFeed combineFeeds(SyndFeed originalFeed, SyndFeed newFeed) { + SyndFeed result; + try { + result = (SyndFeed) newFeed.clone(); + + result.getEntries().addAll(result.getEntries().size(), originalFeed.getEntries()); + + return result; + } catch (CloneNotSupportedException e) { + IllegalArgumentException iae = new IllegalArgumentException("Cannot clone feed"); + iae.initCause(e); + throw iae; + } + } + + public boolean isPreserveWireFeed() { + return preserveWireFeed; + } + + public void setPreserveWireFeed(boolean preserveWireFeed) { + this.preserveWireFeed = preserveWireFeed; + } + +} diff --git a/src/java/org/rometools/fetcher/impl/AbstractFeedFetcherBeanInfo.java b/src/java/org/rometools/fetcher/impl/AbstractFeedFetcherBeanInfo.java new file mode 100644 index 0000000..7342901 --- /dev/null +++ b/src/java/org/rometools/fetcher/impl/AbstractFeedFetcherBeanInfo.java @@ -0,0 +1,29 @@ +package org.rometools.fetcher.impl; + +import java.beans.EventSetDescriptor; +import java.beans.SimpleBeanInfo; +import java.lang.reflect.Method; + +import org.rometools.fetcher.FetcherEvent; +import org.rometools.fetcher.FetcherListener; + +public class AbstractFeedFetcherBeanInfo extends SimpleBeanInfo { + + public EventSetDescriptor[] getEventSetDescriptors() { + try { + Class clz = AbstractFeedFetcher.class; // get the class object which we'll describe + Method addMethod = clz.getMethod("addFetcherEventListener", new Class[] { FetcherListener.class }); + Method removeMethod = clz.getMethod("removeFetcherEventListener", new Class[] { FetcherListener.class }); + Method listenerMethod = FetcherListener.class.getMethod("fetcherEvent", new Class[] { FetcherEvent.class }); + + EventSetDescriptor est = new EventSetDescriptor("fetcherEvent", clz, new Method[] { listenerMethod }, addMethod, removeMethod); + EventSetDescriptor[] results = new EventSetDescriptor[] { est }; + + return results; + } catch (Exception e) { + // IntrospectionException, SecurityException and/or NoSuchMethodException can be thrown here + // the best we can do is to convert them to runtime exceptions + throw new RuntimeException(e); + } + } +} diff --git a/src/java/org/rometools/fetcher/impl/DiskFeedInfoCache.java b/src/java/org/rometools/fetcher/impl/DiskFeedInfoCache.java new file mode 100644 index 0000000..df788bc --- /dev/null +++ b/src/java/org/rometools/fetcher/impl/DiskFeedInfoCache.java @@ -0,0 +1,134 @@ +/* + * Copyright 2005 Sun Microsystems, Inc. + * + * Licensed 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.rometools.fetcher.impl; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.net.URL; +import javax.swing.text.Utilities; + + +/** + * Disk based cache. + */ +public class DiskFeedInfoCache implements FeedFetcherCache { + + protected String cachePath = null; + public DiskFeedInfoCache(String cachePath) { + this.cachePath = cachePath; + } + public SyndFeedInfo getFeedInfo(URL url) { + SyndFeedInfo info = null; + String fileName = cachePath + File.separator + "feed_" + + replaceNonAlphanumeric(url.toString(),'_').trim(); + FileInputStream fis; + try { + fis = new FileInputStream(fileName); + ObjectInputStream ois = new ObjectInputStream(fis); + info = (SyndFeedInfo)ois.readObject(); + fis.close(); + } catch (FileNotFoundException fnfe) { + // That's OK, we'l return null + } catch (ClassNotFoundException cnfe) { + // Error writing to cache is fatal + throw new RuntimeException("Attempting to read from cache", cnfe); + } catch (IOException fnfe) { + // Error writing to cache is fatal + throw new RuntimeException("Attempting to read from cache", fnfe); + } + return info; + } + + public void setFeedInfo(URL url, SyndFeedInfo feedInfo) { + String fileName = cachePath + File.separator + "feed_" + + replaceNonAlphanumeric(url.toString(),'_').trim(); + FileOutputStream fos; + try { + fos = new FileOutputStream(fileName); + ObjectOutputStream oos = new ObjectOutputStream(fos); + oos.writeObject(feedInfo); + fos.flush(); + fos.close(); + } catch (Exception e) { + // Error writing to cache is fatal + throw new RuntimeException("Attempting to write to cache", e); + } + } + + public static String replaceNonAlphanumeric(String str, char subst) { + StringBuffer ret = new StringBuffer(str.length()); + char[] testChars = str.toCharArray(); + for (int i = 0; i < testChars.length; i++) { + if (Character.isLetterOrDigit(testChars[i])) { + ret.append(testChars[i]); + } else { + ret.append( subst ); + } + } + return ret.toString(); + } + + /** + * Clear the cache. + */ + public synchronized void clear() { + final File file = new File(this.cachePath); + //only do the delete if the directory exists + if( file.exists() && file.canWrite() ) { + //make the directory empty + final String[] files = file.list(); + final int len = files.length; + for( int i=0; iA very simple implementation of the {@link com.sun.syndication.fetcher.impl.FeedFetcherCache} interface.
+ * + *This implementation uses a HashMap to cache retrieved feeds. This implementation is + * most suitible for sort term (client aggregator?) use, as the memory usage will increase + * over time as the number of feeds in the cache increases.
+ * + * @author Nick Lothian + * + */ +public class HashMapFeedInfoCache implements FeedFetcherCache, Serializable { + private static final long serialVersionUID = -1594665619950916222L; + + static HashMapFeedInfoCache _instance; + + private Map infoCache; + + /** + *Constructor for HashMapFeedInfoCache
+ * + *Only use this if you want multiple instances of the cache. + * Usually getInstance() is more appropriate.
+ * + */ + public HashMapFeedInfoCache() { + setInfoCache(createInfoCache()); + } + + /** + * Get the global instance of the cache + * @return an implementation of FeedFetcherCache + */ + public static synchronized FeedFetcherCache getInstance() { + if (_instance == null) { + _instance = new HashMapFeedInfoCache(); + } + return _instance; + } + + protected Map createInfoCache() { + return (Collections.synchronizedMap(new HashMap())); + } + + + protected Object get(Object key) { + return getInfoCache().get(key); + } + + /** + * @see extensions.io.FeedFetcherCache#getFeedInfo(java.net.URL) + */ + public SyndFeedInfo getFeedInfo(URL feedUrl) { + return (SyndFeedInfo) get(feedUrl.toString()); + } + + protected void put(Object key, Object value) { + getInfoCache().put(key, value); + } + + /** + * @see extensions.io.FeedFetcherCache#setFeedInfo(java.net.URL, extensions.io.SyndFeedInfo) + */ + public void setFeedInfo(URL feedUrl, SyndFeedInfo syndFeedInfo) { + put(feedUrl.toString(), syndFeedInfo); + } + + protected synchronized final Map getInfoCache() { + return infoCache; + } + + /** + * The API of this class indicates that map must thread safe. In other + * words, be sure to wrap it in a synchronized map unless you know + * what you are doing. + * + * @param map the map to use as the info cache. + */ + protected synchronized final void setInfoCache(Map map) { + infoCache = map; + } + + /** + * @see com.sun.syndication.fetcher.impl.FeedFetcherCache#clear() + */ + public void clear() { + synchronized( infoCache ) { + infoCache.clear(); + } + } + + /** + * @see com.sun.syndication.fetcher.impl.FeedFetcherCache#remove(java.net.URL) + */ + public SyndFeedInfo remove( final URL url ) { + if( url == null ) return null; + + return (SyndFeedInfo) infoCache.remove( url.toString() ); + } + +} diff --git a/src/java/org/rometools/fetcher/impl/HttpClientFeedFetcher.java b/src/java/org/rometools/fetcher/impl/HttpClientFeedFetcher.java new file mode 100644 index 0000000..e47003c --- /dev/null +++ b/src/java/org/rometools/fetcher/impl/HttpClientFeedFetcher.java @@ -0,0 +1,379 @@ +/* + * Copyright 2004 Sun Microsystems, Inc. + * + * Licensed 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.rometools.fetcher.impl; + +import com.sun.syndication.feed.synd.SyndFeed; +import org.rometools.fetcher.FetcherEvent; +import org.rometools.fetcher.FetcherException; +import com.sun.syndication.io.FeedException; +import com.sun.syndication.io.SyndFeedInput; +import com.sun.syndication.io.XmlReader; + +import org.apache.commons.httpclient.Credentials; +import org.apache.commons.httpclient.Header; +import org.apache.commons.httpclient.HttpClient; +import org.apache.commons.httpclient.HttpException; +import org.apache.commons.httpclient.HttpMethod; +import org.apache.commons.httpclient.methods.GetMethod; +import org.apache.commons.httpclient.params.HttpClientParams; + +import java.io.IOException; +import java.io.InputStream; + +import java.net.HttpURLConnection; +import java.net.MalformedURLException; +import java.net.URL; + +import java.util.zip.GZIPInputStream; + + +/** + * @author Nick Lothian + */ +public class HttpClientFeedFetcher extends AbstractFeedFetcher { + private CredentialSupplier credentialSupplier; + private FeedFetcherCache feedInfoCache; + private volatile HttpClientMethodCallbackIntf httpClientMethodCallback; + private volatile HttpClientParams httpClientParams; + + public HttpClientFeedFetcher() { + super(); + setHttpClientParams(new HttpClientParams()); + } + + /** + * @param cache + */ + public HttpClientFeedFetcher(FeedFetcherCache cache) { + this(); + setFeedInfoCache(cache); + } + + public HttpClientFeedFetcher(FeedFetcherCache cache, CredentialSupplier credentialSupplier) { + this(cache); + setCredentialSupplier(credentialSupplier); + } + + /** + * @param timeout Sets the connect timeout for the HttpClient but using the URLConnection method name. + * Uses the HttpClientParams method setConnectionManagerTimeout instead of setConnectTimeout + * + */ + public synchronized void setConnectTimeout(int timeout) { + httpClientParams.setConnectionManagerTimeout(timeout); + } + + /** + * @return The currently used connect timeout for the HttpClient but using the URLConnection method name. + * Uses the HttpClientParams method getConnectionManagerTimeout instead of getConnectTimeout + * + */ + public int getConnectTimeout() { + return (int) this.getHttpClientParams() + .getConnectionManagerTimeout(); + } + + /** + * @param credentialSupplier The credentialSupplier to set. + */ + public synchronized void setCredentialSupplier(CredentialSupplier credentialSupplier) { + this.credentialSupplier = credentialSupplier; + } + + /** + * @return Returns the credentialSupplier. + */ + public synchronized CredentialSupplier getCredentialSupplier() { + return credentialSupplier; + } + + /** + * @param feedInfoCache the feedInfoCache to set + */ + public synchronized void setFeedInfoCache(FeedFetcherCache feedInfoCache) { + this.feedInfoCache = feedInfoCache; + } + + /** + * @return the feedInfoCache. + */ + public synchronized FeedFetcherCache getFeedInfoCache() { + return feedInfoCache; + } + + public synchronized void setHttpClientMethodCallback(HttpClientMethodCallbackIntf httpClientMethodCallback) { + this.httpClientMethodCallback = httpClientMethodCallback; + } + + public HttpClientMethodCallbackIntf getHttpClientMethodCallback() { + return httpClientMethodCallback; + } + + /** + * @param httpClientParams The httpClientParams to set. + */ + public synchronized void setHttpClientParams(HttpClientParams httpClientParams) { + this.httpClientParams = httpClientParams; + } + + /** + * @return Returns the httpClientParams. + */ + public synchronized HttpClientParams getHttpClientParams() { + return this.httpClientParams; + } + + /** + * @return The currently used read timeout for the URLConnection, 0 is unlimited, i.e. no timeout + */ + public synchronized void setReadTimeout(int timeout) { + httpClientParams.setSoTimeout(timeout); + } + + /** + * @param timeout Sets the read timeout for the URLConnection to a specified timeout, in milliseconds. + */ + public int getReadTimeout() { + return (int) this.getHttpClientParams() + .getSoTimeout(); + } + + public SyndFeed retrieveFeed(URL url) throws IllegalArgumentException, IOException, FeedException, FetcherException { + return this.retrieveFeed(this.getUserAgent(), url); + } + + /** + * @see com.sun.syndication.fetcher.FeedFetcher#retrieveFeed(java.net.URL) + */ + public SyndFeed retrieveFeed(String userAgent, URL feedUrl) + throws IllegalArgumentException, IOException, FeedException, FetcherException { + if (feedUrl == null) { + throw new IllegalArgumentException("null is not a valid URL"); + } + + // TODO Fix this + //System.setProperty("org.apache.commons.logging.Log", "org.apache.commons.logging.impl.SimpleLog"); + HttpClient client = new HttpClient(httpClientParams); + + if (getCredentialSupplier() != null) { + client.getState() + .setAuthenticationPreemptive(true); + + // TODO what should realm be here? + Credentials credentials = getCredentialSupplier() + .getCredentials(null, feedUrl.getHost()); + + if (credentials != null) { + client.getState() + .setCredentials(null, feedUrl.getHost(), credentials); + } + } + + System.setProperty("httpclient.useragent", userAgent); + + String urlStr = feedUrl.toString(); + + HttpMethod method = new GetMethod(urlStr); + method.addRequestHeader("Accept-Encoding", "gzip"); + method.addRequestHeader("User-Agent", userAgent); + method.setFollowRedirects(true); + + if (httpClientMethodCallback != null) { + synchronized (httpClientMethodCallback) { + httpClientMethodCallback.afterHttpClientMethodCreate(method); + } + } + + FeedFetcherCache cache = getFeedInfoCache(); + + if (cache != null) { + // retrieve feed + try { + if (isUsingDeltaEncoding()) { + method.setRequestHeader("A-IM", "feed"); + } + + // get the feed info from the cache + // Note that syndFeedInfo will be null if it is not in the cache + SyndFeedInfo syndFeedInfo = cache.getFeedInfo(feedUrl); + + if (syndFeedInfo != null) { + method.setRequestHeader("If-None-Match", syndFeedInfo.getETag()); + + if (syndFeedInfo.getLastModified() instanceof String) { + method.setRequestHeader("If-Modified-Since", (String) syndFeedInfo.getLastModified()); + } + } + + int statusCode = client.executeMethod(method); + fireEvent(FetcherEvent.EVENT_TYPE_FEED_POLLED, urlStr); + handleErrorCodes(statusCode); + + SyndFeed feed = getFeed(syndFeedInfo, urlStr, method, statusCode); + + syndFeedInfo = buildSyndFeedInfo(feedUrl, urlStr, method, feed, statusCode); + + cache.setFeedInfo(new URL(urlStr), syndFeedInfo); + + // the feed may have been modified to pick up cached values + // (eg - for delta encoding) + feed = syndFeedInfo.getSyndFeed(); + + return feed; + } finally { + method.releaseConnection(); + method.recycle(); + } + } else { + // cache is not in use + try { + int statusCode = client.executeMethod(method); + fireEvent(FetcherEvent.EVENT_TYPE_FEED_POLLED, urlStr); + handleErrorCodes(statusCode); + + return getFeed(null, urlStr, method, statusCode); + } finally { + method.releaseConnection(); + method.recycle(); + } + } + } + + private SyndFeed getFeed(SyndFeedInfo syndFeedInfo, String urlStr, HttpMethod method, int statusCode) + throws IOException, HttpException, FetcherException, FeedException { + if ((statusCode == HttpURLConnection.HTTP_NOT_MODIFIED) && (syndFeedInfo != null)) { + fireEvent(FetcherEvent.EVENT_TYPE_FEED_UNCHANGED, urlStr); + + return syndFeedInfo.getSyndFeed(); + } + + SyndFeed feed = retrieveFeed(urlStr, method); + fireEvent(FetcherEvent.EVENT_TYPE_FEED_RETRIEVED, urlStr, feed); + + return feed; + } + + /** + * @param feedUrl + * @param urlStr + * @param method + * @param feed + * @return + * @throws MalformedURLException + */ + private SyndFeedInfo buildSyndFeedInfo(URL feedUrl, String urlStr, HttpMethod method, SyndFeed feed, int statusCode) + throws MalformedURLException { + SyndFeedInfo syndFeedInfo; + syndFeedInfo = new SyndFeedInfo(); + + // this may be different to feedURL because of 3XX redirects + syndFeedInfo.setUrl(new URL(urlStr)); + syndFeedInfo.setId(feedUrl.toString()); + + Header imHeader = method.getResponseHeader("IM"); + + if ((imHeader != null) && (imHeader.getValue() + .indexOf("feed") >= 0) && isUsingDeltaEncoding()) { + FeedFetcherCache cache = getFeedInfoCache(); + + if ((cache != null) && (statusCode == 226)) { + // client is setup to use http delta encoding and the server supports it and has returned a delta encoded response + // This response only includes new items + SyndFeedInfo cachedInfo = cache.getFeedInfo(feedUrl); + + if (cachedInfo != null) { + SyndFeed cachedFeed = cachedInfo.getSyndFeed(); + + // set the new feed to be the orginal feed plus the new items + feed = combineFeeds(cachedFeed, feed); + } + } + } + + Header lastModifiedHeader = method.getResponseHeader("Last-Modified"); + + if (lastModifiedHeader != null) { + syndFeedInfo.setLastModified(lastModifiedHeader.getValue()); + } + + Header eTagHeader = method.getResponseHeader("ETag"); + + if (eTagHeader != null) { + syndFeedInfo.setETag(eTagHeader.getValue()); + } + + syndFeedInfo.setSyndFeed(feed); + + return syndFeedInfo; + } + + /** + * @param client + * @param urlStr + * @param method + * @return + * @throws IOException + * @throws HttpException + * @throws FetcherException + * @throws FeedException + */ + private SyndFeed retrieveFeed(String urlStr, HttpMethod method) + throws IOException, HttpException, FetcherException, FeedException { + InputStream stream = null; + + if ((method.getResponseHeader("Content-Encoding") != null) && + ("gzip".equalsIgnoreCase(method.getResponseHeader("Content-Encoding").getValue()))) { + stream = new GZIPInputStream(method.getResponseBodyAsStream()); + } else { + stream = method.getResponseBodyAsStream(); + } + + try { + XmlReader reader = null; + + if (method.getResponseHeader("Content-Type") != null) { + reader = new XmlReader(stream, method.getResponseHeader("Content-Type").getValue(), true); + } else { + reader = new XmlReader(stream, true); + } + + SyndFeedInput syndFeedInput = new SyndFeedInput(); + syndFeedInput.setPreserveWireFeed(isPreserveWireFeed()); + + return syndFeedInput.build(reader); + } finally { + if (stream != null) { + stream.close(); + } + } + } + + public interface CredentialSupplier { + public Credentials getCredentials(String realm, String host); + } + + public interface HttpClientMethodCallbackIntf { + /** + * Allows access to the underlying HttpClient HttpMethod object. + * Note that in most cases, method.setRequestHeader(String, String) + * is what you want to do (rather than method.addRequestHeader(String, String)) + * + * @param method + */ + public void afterHttpClientMethodCreate(HttpMethod method); + } +} diff --git a/src/java/org/rometools/fetcher/impl/HttpURLFeedFetcher.java b/src/java/org/rometools/fetcher/impl/HttpURLFeedFetcher.java new file mode 100644 index 0000000..67981b1 --- /dev/null +++ b/src/java/org/rometools/fetcher/impl/HttpURLFeedFetcher.java @@ -0,0 +1,299 @@ +/* + * Copyright 2004 Sun Microsystems, Inc. + * + * Licensed 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.rometools.fetcher.impl; + +import java.io.BufferedInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.HttpURLConnection; +import java.net.URL; +import java.net.URLConnection; +import java.util.zip.GZIPInputStream; + +import com.sun.syndication.feed.synd.SyndFeed; +import org.rometools.fetcher.FetcherEvent; +import org.rometools.fetcher.FetcherException; +import com.sun.syndication.io.FeedException; +import com.sun.syndication.io.SyndFeedInput; +import com.sun.syndication.io.XmlReader; + +/** + *Class to retrieve syndication files via HTTP.
+ * + *If passed a {@link com.sun.syndication.fetcher.impl.FeedFetcherCache} in the + * constructor it will use conditional gets to only retrieve modified content.
+ * + *The class uses the Accept-Encoding: gzip header to retrieve gzipped feeds where + * supported by the server.
+ * + *Simple usage: + *
+ * // create the cache + * FeedFetcherCache feedInfoCache = HashMapFeedInfoCache.getFeedInfoCache(); + * // retrieve the feed the first time + * // any subsequent request will use conditional gets and only + * // retrieve the resource if it has changed + * SyndFeed feed = new HttpURLFeedFetcher(feedInfoCache).retrieveFeed(feedUrl); + *+ * + * + * + * @see http://fishbowl.pastiche.org/2002/10/21/http_conditional_get_for_rss_hackers + * @see http://diveintomark.org/archives/2003/07/21/atom_aggregator_behavior_http_level + * @see http://bobwyman.pubsub.com/main/2004/09/using_rfc3229_w.html + * @author Nick Lothian + */ +public class HttpURLFeedFetcher extends AbstractFeedFetcher { + static final int POLL_EVENT = 1; + static final int RETRIEVE_EVENT = 2; + static final int UNCHANGED_EVENT = 3; + + private FeedFetcherCache feedInfoCache; + + + /** + * Constructor to use HttpURLFeedFetcher without caching of feeds + * + */ + public HttpURLFeedFetcher() { + super(); + } + + /** + * Constructor to enable HttpURLFeedFetcher to cache feeds + * + * @param feedCache - an instance of the FeedFetcherCache interface + */ + public HttpURLFeedFetcher(FeedFetcherCache feedInfoCache) { + this(); + setFeedInfoCache(feedInfoCache); + } + + public SyndFeed retrieveFeed(URL feedUrl) throws IllegalArgumentException, IOException, FeedException, FetcherException { + return this.retrieveFeed(this.getUserAgent(), feedUrl); + } + + /** + * Retrieve a feed over HTTP + * + * @param feedUrl A non-null URL of a RSS/Atom feed to retrieve + * @return A {@link com.sun.syndication.feed.synd.SyndFeed} object + * @throws IllegalArgumentException if the URL is null; + * @throws IOException if a TCP error occurs + * @throws FeedException if the feed is not valid + * @throws FetcherException if a HTTP error occurred + */ + public SyndFeed retrieveFeed(String userAgent, URL feedUrl) throws IllegalArgumentException, IOException, FeedException, FetcherException { + if (feedUrl == null) { + throw new IllegalArgumentException("null is not a valid URL"); + } + + URLConnection connection = feedUrl.openConnection(); + if (!(connection instanceof HttpURLConnection)) { + throw new IllegalArgumentException(feedUrl.toExternalForm() + " is not a valid HTTP Url"); + } + HttpURLConnection httpConnection = (HttpURLConnection)connection; + // httpConnection.setInstanceFollowRedirects(true); // this is true by default, but can be changed on a claswide basis + + FeedFetcherCache cache = getFeedInfoCache(); + if (cache != null) { + SyndFeedInfo syndFeedInfo = cache.getFeedInfo(feedUrl); + setRequestHeaders(connection, syndFeedInfo); + httpConnection.connect(); + try { + fireEvent(FetcherEvent.EVENT_TYPE_FEED_POLLED, connection); + + if (syndFeedInfo == null) { + // this is a feed that hasn't been retrieved + syndFeedInfo = new SyndFeedInfo(); + retrieveAndCacheFeed(feedUrl, syndFeedInfo, httpConnection); + } else { + // check the response code + int responseCode = httpConnection.getResponseCode(); + if (responseCode != HttpURLConnection.HTTP_NOT_MODIFIED) { + // the response code is not 304 NOT MODIFIED + // This is either because the feed server + // does not support condition gets + // or because the feed hasn't changed + retrieveAndCacheFeed(feedUrl, syndFeedInfo, httpConnection); + } else { + // the feed does not need retrieving + fireEvent(FetcherEvent.EVENT_TYPE_FEED_UNCHANGED, connection); + } + } + + return syndFeedInfo.getSyndFeed(); + } finally { + httpConnection.disconnect(); + } + } else { + fireEvent(FetcherEvent.EVENT_TYPE_FEED_POLLED, connection); + InputStream inputStream = null; + setRequestHeaders(connection, null); + + connection.addRequestProperty("User-Agent", userAgent); + + httpConnection.connect(); + try { + inputStream = httpConnection.getInputStream(); + return getSyndFeedFromStream(inputStream, connection); + } catch (java.io.IOException e) { + handleErrorCodes(((HttpURLConnection)connection).getResponseCode()); + } finally { + if (inputStream != null) { + inputStream.close(); + } + httpConnection.disconnect(); + } + // we will never actually get to this line + return null; + } + } + + protected void retrieveAndCacheFeed(URL feedUrl, SyndFeedInfo syndFeedInfo, HttpURLConnection connection) throws IllegalArgumentException, FeedException, FetcherException, IOException { + handleErrorCodes(connection.getResponseCode()); + + resetFeedInfo(feedUrl, syndFeedInfo, connection); + FeedFetcherCache cache = getFeedInfoCache(); + // resetting feed info in the cache + // could be needed for some implementations + // of FeedFetcherCache (eg, distributed HashTables) + if (cache != null) { + cache.setFeedInfo(feedUrl, syndFeedInfo); + } + } + + protected void resetFeedInfo(URL orignalUrl, SyndFeedInfo syndFeedInfo, HttpURLConnection connection) throws IllegalArgumentException, IOException, FeedException { + // need to always set the URL because this may have changed due to 3xx redirects + syndFeedInfo.setUrl(connection.getURL()); + + // the ID is a persistant value that should stay the same even if the URL for the + // feed changes (eg, by 3xx redirects) + syndFeedInfo.setId(orignalUrl.toString()); + + // This will be 0 if the server doesn't support or isn't setting the last modified header + syndFeedInfo.setLastModified(new Long(connection.getLastModified())); + + // This will be null if the server doesn't support or isn't setting the ETag header + syndFeedInfo.setETag(connection.getHeaderField("ETag")); + + // get the contents + InputStream inputStream = null; + try { + inputStream = connection.getInputStream(); + SyndFeed syndFeed = getSyndFeedFromStream(inputStream, connection); + + String imHeader = connection.getHeaderField("IM"); + if (isUsingDeltaEncoding() && (imHeader!= null && imHeader.indexOf("feed") >= 0)) { + FeedFetcherCache cache = getFeedInfoCache(); + if (cache != null && connection.getResponseCode() == 226) { + // client is setup to use http delta encoding and the server supports it and has returned a delta encoded response + // This response only includes new items + SyndFeedInfo cachedInfo = cache.getFeedInfo(orignalUrl); + if (cachedInfo != null) { + SyndFeed cachedFeed = cachedInfo.getSyndFeed(); + + // set the new feed to be the orginal feed plus the new items + syndFeed = combineFeeds(cachedFeed, syndFeed); + } + } + } + + syndFeedInfo.setSyndFeed(syndFeed); + } finally { + if (inputStream != null) { + inputStream.close(); + } + } + } + + /** + *
Set appropriate HTTP headers, including conditional get and gzip encoding headers
+ * + * @param connection A URLConnection + * @param syndFeedInfo The SyndFeedInfo for the feed to be retrieved. May be null + */ + protected void setRequestHeaders(URLConnection connection, SyndFeedInfo syndFeedInfo) { + if (syndFeedInfo != null) { + // set the headers to get feed only if modified + // we support the use of both last modified and eTag headers + if (syndFeedInfo.getLastModified() != null) { + Object lastModified = syndFeedInfo.getLastModified(); + if (lastModified instanceof Long) { + connection.setIfModifiedSince(((Long)syndFeedInfo.getLastModified()).longValue()); + } + } + if (syndFeedInfo.getETag() != null) { + connection.setRequestProperty("If-None-Match", syndFeedInfo.getETag()); + } + + } + // header to retrieve feed gzipped + connection.setRequestProperty("Accept-Encoding", "gzip"); + + if (isUsingDeltaEncoding()) { + connection.addRequestProperty("A-IM", "feed"); + } + } + + private SyndFeed readSyndFeedFromStream(InputStream inputStream, URLConnection connection) throws IOException, IllegalArgumentException, FeedException { + BufferedInputStream is; + if ("gzip".equalsIgnoreCase(connection.getContentEncoding())) { + // handle gzip encoded content + is = new BufferedInputStream(new GZIPInputStream(inputStream)); + } else { + is = new BufferedInputStream(inputStream); + } + + //InputStreamReader reader = new InputStreamReader(is, ResponseHandler.getCharacterEncoding(connection)); + + //SyndFeedInput input = new SyndFeedInput(); + + XmlReader reader = null; + if (connection.getHeaderField("Content-Type") != null) { + reader = new XmlReader(is, connection.getHeaderField("Content-Type"), true); + } else { + reader = new XmlReader(is, true); + } + + SyndFeedInput syndFeedInput = new SyndFeedInput(); + syndFeedInput.setPreserveWireFeed(isPreserveWireFeed()); + + return syndFeedInput.build(reader); + + } + + private SyndFeed getSyndFeedFromStream(InputStream inputStream, URLConnection connection) throws IOException, IllegalArgumentException, FeedException { + SyndFeed feed = readSyndFeedFromStream(inputStream, connection); + fireEvent(FetcherEvent.EVENT_TYPE_FEED_RETRIEVED, connection, feed); + return feed; + } + + /** + * @return The FeedFetcherCache used by this fetcher (Could be null) + */ + public synchronized FeedFetcherCache getFeedInfoCache() { + return feedInfoCache; + } + + /** + * @param cache The cache to be used by this fetcher (pass null to stop using a cache) + */ + public synchronized void setFeedInfoCache(FeedFetcherCache cache) { + feedInfoCache = cache; + } +} diff --git a/src/java/org/rometools/fetcher/impl/LinkedHashMapFeedInfoCache.java b/src/java/org/rometools/fetcher/impl/LinkedHashMapFeedInfoCache.java new file mode 100644 index 0000000..c1eab76 --- /dev/null +++ b/src/java/org/rometools/fetcher/impl/LinkedHashMapFeedInfoCache.java @@ -0,0 +1,70 @@ +package org.rometools.fetcher.impl; + +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.Map; + +/** + *An implementation of the {@link com.sun.syndication.fetcher.impl.FeedFetcherCache} interface.
+ * + *Unlike the HashMapFeedInfoCache this implementation will not grow unbound
+ * + * @author Javier Kohen + * @author Nick Lothian + * + */ +public class LinkedHashMapFeedInfoCache extends HashMapFeedInfoCache { + private final class CacheImpl extends LinkedHashMap { + private static final long serialVersionUID = -6977191330127794920L; + + public CacheImpl() { + super(16, 0.75F, true); + } + + protected boolean removeEldestEntry(Map.Entry eldest) { + return size() > getMaxEntries(); + } + } + + private static final int DEFAULT_MAX_ENTRIES = 20; + + private static final long serialVersionUID = 1694228973357997417L; + + private int maxEntries = DEFAULT_MAX_ENTRIES; + + private final static LinkedHashMapFeedInfoCache _instance = new LinkedHashMapFeedInfoCache(); + + + /** + * Get the global instance of the cache + * @return an implementation of FeedFetcherCache + */ + public static final FeedFetcherCache getInstance() { + return _instance; + } + + /** + *Constructor for HashMapFeedInfoCache
+ * + *Only use this if you want multiple instances of the cache. + * Usually {@link #getInstance()} is more appropriate.
+ * + * @see #getInstance() + */ + public LinkedHashMapFeedInfoCache() { + super(); + } + + protected Map createInfoCache() { + return Collections.synchronizedMap(new CacheImpl()); + } + + public synchronized final int getMaxEntries() { + return maxEntries; + } + + public synchronized final void setMaxEntries(int maxEntries) { + this.maxEntries = maxEntries; + } + +} diff --git a/src/java/org/rometools/fetcher/impl/ResponseHandler.java b/src/java/org/rometools/fetcher/impl/ResponseHandler.java new file mode 100644 index 0000000..952edf3 --- /dev/null +++ b/src/java/org/rometools/fetcher/impl/ResponseHandler.java @@ -0,0 +1,57 @@ +/* + * Copyright 2004 Sun Microsystems, Inc. + * + * Licensed 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.rometools.fetcher.impl; + +import java.net.URLConnection; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Utility class to help deal with HTTP responses + * + */ +public class ResponseHandler { + public static final String defaultCharacterEncoding = "ISO-8859-1"; + + private final static Pattern characterEncodingPattern = Pattern.compile("charset=([.[^; ]]*)"); + + public static String getCharacterEncoding(URLConnection connection) { + return getCharacterEncoding(connection.getContentType()); + } + + /** + * + *Gets the character encoding of a response. (Note that this is different to + * the content-encoding)
+ * + * @param contentTypeHeader the value of the content-type HTTP header eg: text/html; charset=ISO-8859-4 + * @return the character encoding, eg: ISO-8859-4 + */ + public static String getCharacterEncoding(String contentTypeHeader) { + if (contentTypeHeader == null) { + return defaultCharacterEncoding; + } + + Matcher m = characterEncodingPattern.matcher(contentTypeHeader); + //if (!m.matches()) { + if (!m.find()) { + return defaultCharacterEncoding; + } else { + return m.group(1); + } + } +} diff --git a/src/java/org/rometools/fetcher/impl/SyndFeedInfo.java b/src/java/org/rometools/fetcher/impl/SyndFeedInfo.java new file mode 100644 index 0000000..587f309 --- /dev/null +++ b/src/java/org/rometools/fetcher/impl/SyndFeedInfo.java @@ -0,0 +1,149 @@ +/* + * Copyright 2004 Sun Microsystems, Inc. + * + * Licensed 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.rometools.fetcher.impl; + +import java.io.Serializable; +import java.net.URL; + +import com.sun.syndication.feed.impl.ObjectBean; +import com.sun.syndication.feed.synd.SyndFeed; + +/** + *A class to represent a {@link com.sun.syndication.feed.synd.SyndFeed} + * and some useful information about it.
+ * + *This class is thread safe, as expected by the different feed fetcher + * implementations.
+ * + * @author Nick Lothian + */ +public class SyndFeedInfo implements Cloneable, Serializable { + private static final long serialVersionUID = -1874786860901426015L; + + private final ObjectBean _objBean; + private String id; + private URL url; + private Object lastModified; + private String eTag; + private SyndFeed syndFeed; + + public SyndFeedInfo() { + _objBean = new ObjectBean(this.getClass(),this); + } + + /** + * Creates a deep 'bean' clone of the object. + *+ * @return a clone of the object. + * @throws CloneNotSupportedException thrown if an element of the object cannot be cloned. + * + */ + public Object clone() throws CloneNotSupportedException { + return _objBean.clone(); + } + + /** + * Indicates whether some other object is "equal to" this one as defined by the Object equals() method. + *
+ * @param other he reference object with which to compare. + * @return true if 'this' object is equal to the 'other' object. + * + */ + public boolean equals(Object other) { + return _objBean.equals(other); + } + + /** + * Returns a hashcode value for the object. + *
+ * It follows the contract defined by the Object hashCode() method. + *
+ * @return the hashcode of the bean object. + * + */ + public int hashCode() { + return _objBean.hashCode(); + } + + /** + * Returns the String representation for the object. + *
+ * @return String representation for the object. + * + */ + public String toString() { + return _objBean.toString(); + } + + + /** + * @return the ETag the feed was last retrieved with + */ + public synchronized String getETag() { + return eTag; + } + + /** + * @return the last modified date for the feed + */ + public synchronized Object getLastModified() { + return lastModified; + } + + /** + * @return the URL the feed was served from + */ + public synchronized URL getUrl() { + return url; + } + + public synchronized void setETag(String string) { + eTag = string; + } + + public synchronized void setLastModified(Object o) { + lastModified = o; + } + + public synchronized void setUrl(URL url) { + this.url = url; + } + + public synchronized SyndFeed getSyndFeed() { + return syndFeed; + } + + public synchronized void setSyndFeed(SyndFeed feed) { + syndFeed = feed; + } + + /** + * @return A unique ID to identify the feed + */ + public synchronized String getId() { + return id; + } + + /** + * @param string A unique ID to identify the feed. Note that if the URL of the feed + * changes this will remain the same + */ + public synchronized void setId(String string) { + id = string; + } + +} diff --git a/src/java/org/rometools/fetcher/samples/FeedAggregator.java b/src/java/org/rometools/fetcher/samples/FeedAggregator.java new file mode 100644 index 0000000..090a0c0 --- /dev/null +++ b/src/java/org/rometools/fetcher/samples/FeedAggregator.java @@ -0,0 +1,92 @@ +/* + * Copyright 2004 Sun Microsystems, Inc. + * + * Licensed 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.rometools.fetcher.samples; + +import java.io.PrintWriter; +import java.net.URL; +import java.util.ArrayList; +import java.util.List; + +import com.sun.syndication.feed.synd.SyndFeedImpl; +import com.sun.syndication.feed.synd.SyndFeed; +import org.rometools.fetcher.FeedFetcher; +import org.rometools.fetcher.impl.FeedFetcherCache; +import org.rometools.fetcher.impl.HashMapFeedInfoCache; +import org.rometools.fetcher.impl.HttpURLFeedFetcher; +import com.sun.syndication.io.SyndFeedOutput; + +/** + *
It aggregates a list of RSS/Atom feeds (they can be of different types) + * into a single feed of the specified type.
+ * + *Converted from the original FeedAggregator sample
+ * + * @author Alejandro Abdelnur + * @author Nick Lothian + * + */ +public class FeedAggregator { + + public static void main(String[] args) { + boolean ok = false; + if (args.length>=2) { + try { + String outputType = args[0]; + + SyndFeed feed = new SyndFeedImpl(); + feed.setFeedType(outputType); + + feed.setTitle("Aggregated Feed"); + feed.setDescription("Anonymous Aggregated Feed"); + feed.setAuthor("anonymous"); + feed.setLink("http://www.anonymous.com"); + + List entries = new ArrayList(); + feed.setEntries(entries); + + FeedFetcherCache feedInfoCache = HashMapFeedInfoCache.getInstance(); + FeedFetcher feedFetcher = new HttpURLFeedFetcher(feedInfoCache); + + for (int i=1;iAn interface to allow caching of feed details. Implementing this allows the - * {@link com.sun.syndication.fetcher.io.HttpURLFeedFetcher} class to + * {@link org.rometools.fetcher.io.HttpURLFeedFetcher} class to * enable conditional gets
* * @author Nick Lothian diff --git a/src/java/org/rometools/fetcher/impl/HashMapFeedInfoCache.java b/src/java/org/rometools/fetcher/impl/HashMapFeedInfoCache.java index 1d8f93f..5f60825 100644 --- a/src/java/org/rometools/fetcher/impl/HashMapFeedInfoCache.java +++ b/src/java/org/rometools/fetcher/impl/HashMapFeedInfoCache.java @@ -24,7 +24,7 @@ import java.util.Map; /** - *A very simple implementation of the {@link com.sun.syndication.fetcher.impl.FeedFetcherCache} interface.
+ *A very simple implementation of the {@link org.rometools.fetcher.impl.FeedFetcherCache} interface.
* *This implementation uses a HashMap to cache retrieved feeds. This implementation is * most suitible for sort term (client aggregator?) use, as the memory usage will increase diff --git a/src/java/org/rometools/fetcher/impl/HttpClientFeedFetcher.java b/src/java/org/rometools/fetcher/impl/HttpClientFeedFetcher.java index e47003c..4d3b8bd 100644 --- a/src/java/org/rometools/fetcher/impl/HttpClientFeedFetcher.java +++ b/src/java/org/rometools/fetcher/impl/HttpClientFeedFetcher.java @@ -17,8 +17,6 @@ package org.rometools.fetcher.impl; import com.sun.syndication.feed.synd.SyndFeed; -import org.rometools.fetcher.FetcherEvent; -import org.rometools.fetcher.FetcherException; import com.sun.syndication.io.FeedException; import com.sun.syndication.io.SyndFeedInput; import com.sun.syndication.io.XmlReader; @@ -31,6 +29,9 @@ import org.apache.commons.httpclient.HttpMethod; import org.apache.commons.httpclient.methods.GetMethod; import org.apache.commons.httpclient.params.HttpClientParams; +import org.rometools.fetcher.FetcherEvent; +import org.rometools.fetcher.FetcherException; + import java.io.IOException; import java.io.InputStream; @@ -145,7 +146,7 @@ public class HttpClientFeedFetcher extends AbstractFeedFetcher { } /** - * @param timeout Sets the read timeout for the URLConnection to a specified timeout, in milliseconds. + * @return timeout the read timeout for the URLConnection to a specified timeout, in milliseconds. */ public int getReadTimeout() { return (int) this.getHttpClientParams() @@ -157,7 +158,7 @@ public class HttpClientFeedFetcher extends AbstractFeedFetcher { } /** - * @see com.sun.syndication.fetcher.FeedFetcher#retrieveFeed(java.net.URL) + * @see org.rometools.fetcher.FeedFetcher#retrieveFeed(java.net.URL) */ public SyndFeed retrieveFeed(String userAgent, URL feedUrl) throws IllegalArgumentException, IOException, FeedException, FetcherException { diff --git a/src/java/org/rometools/fetcher/impl/HttpURLFeedFetcher.java b/src/java/org/rometools/fetcher/impl/HttpURLFeedFetcher.java index 67981b1..6a9026d 100644 --- a/src/java/org/rometools/fetcher/impl/HttpURLFeedFetcher.java +++ b/src/java/org/rometools/fetcher/impl/HttpURLFeedFetcher.java @@ -34,7 +34,7 @@ import com.sun.syndication.io.XmlReader; /** *
Class to retrieve syndication files via HTTP.
* - *If passed a {@link com.sun.syndication.fetcher.impl.FeedFetcherCache} in the + *
If passed a {@link org.rometools.fetcher.impl.FeedFetcherCache} in the * constructor it will use conditional gets to only retrieve modified content.
* *The class uses the Accept-Encoding: gzip header to retrieve gzipped feeds where diff --git a/src/java/org/rometools/fetcher/impl/LinkedHashMapFeedInfoCache.java b/src/java/org/rometools/fetcher/impl/LinkedHashMapFeedInfoCache.java index c1eab76..82abf6b 100644 --- a/src/java/org/rometools/fetcher/impl/LinkedHashMapFeedInfoCache.java +++ b/src/java/org/rometools/fetcher/impl/LinkedHashMapFeedInfoCache.java @@ -5,7 +5,7 @@ import java.util.LinkedHashMap; import java.util.Map; /** - *
An implementation of the {@link com.sun.syndication.fetcher.impl.FeedFetcherCache} interface.
+ *An implementation of the {@link org.rometools.fetcher.impl.FeedFetcherCache} interface.
* *Unlike the HashMapFeedInfoCache this implementation will not grow unbound
* From e365fa78c2dcf463620fb8e3830bd7d9cd7abb7b Mon Sep 17 00:00:00 2001 From: Martin KurzLGae5gqYEU=}IMZ0v=W$!Rs8 z8DwWCI@C01X&93R;8=i@_XM6pqGfX|>z+Klw?!(vk{v4A;BiMwvBaY&V0I?bEuurg zcf2)N)v&%+ETVc1#D{49p^B&$i?ip3%H?#d0(>VVeumT2B((Dyai~PmK$GJm9i5 z*~4E2B)WS6@#daiW7fu2n4+;Uc_BRF-DNie=RLewVS`U7f%8i&CEgI%1dPRtGFRU2 zkkD*PQs>@o&$OG;*L)tGpYL}sZeoY<**IKo4hmz3Xd3CW&2y?xx@!oSQzLLi1#AlN z+)ey_iT84%&blY$$A|< E)Lx1K8`cV^GbqcGy9*kR_iT*Sxt2ki8>KKis8p z_H=a5ixW7G IH3 zt7xYgc5oGGuZZuh^RhMiEBwJz#hoi&ZK3EYrMsndmNW<@th>sSuE9XGV_ax#fyLWM zk@}z(k~IP#yW#M=&;}@Mz;MGbp{d^5EjM?Tbd9KE-tW+n8@Mzy+(^pBm+j%LPwvf7 zV{Fk>48Ll25mfY?euV%_fMG876 (`$9Bb0V(aXAxv6L`#uJ3JS9DmI#xZf>8p^>r z*BO!BQcNR$*LBs8G^~7Z4m7*YTjpsOV;mZVE;^QspITMtSO19+z{<)BsCDVTsTwt; zXnY (M zmiS(yjVfUj* s42_{J4W| zP*B{hbLA#~f_`kh>XZRO!}kbkU$h9IhhB!icm7MKV;WvzMbv$udK3}%{E}!}!3p?Q zLoLslOEH=*BtZ~{GeE`PY_c9(r 4!)zykq9|Bi$hr8_E zd>N1Pbf*v4p4oEJ37#T9gyEEBeaR7ITyrXf9zG^N3n;B3i|ttrx)wRfMycz0q5ums zO2J8^rC4^O-Bh!Kr4E7ydpoEG%cQSm
ibLf1jK}h^HcQUiBm;jkXh(kq-*psBf_)02?P4 z7gtL&)ws`1L9YIoqSx0efoLsl>(246P~yd@i<~fi$ hEhdXD zTD(wxda;Y)CGl~a$X}P6iQ3ZfeN)imDfo$qifOOoZS*6?11BFRFhe9vJIX#hz)qmV z9+26w5R<%7SdL^L>%z4adXJ6g1_f&C%a@nt;^GECt9FQXIOm?WE5nqr`sB#`hqtgQ zGXu3mQ*CO)a5?Q{hlnRUJ{*^dl=Az_wia-lixBdNGf*Yb_?}Wa3{s4@9P4=dodPlw zxoXVROs@^US6GMj`d|qLZipA@eU|kbD82>}k;@IS4uLZ8fa69ipM(qnljhjqM9-0R zH}kJ*=VLct1rplZg1 >K`q#X}`nxlIGOtZ>#BzlWo|A*;-TQ9JOJGIPt5!Qs9G-|L6uC-0 zOE^Gn#hzG>n4Iu@ArWo}m6(Hvw#`JVW+a(d@O*t3=#0mNrti!-L23Z}ODr4O$;#Et zHd|9u(|g=#zthqlNO9aM5aYd_0EOy1xWSUJ4@UyjHK%;FpFT){1rLHb_BgFE#8PN@ z*1+C1rmJz!`UT>=*!-M)5lLg+55GAvZ#s)a6VsM^cN)nOW|UVr=J4F3rXrB%;p7 |B1;|!4n}kHz)Y8kl(SgnlHDdHY$EMLN_itFN3(&V)%U&~T9^15U7Qitt^7zp zsACa|-=Z8I#b34Y3o{g~URa9TT#L3Am#Ua~8V8o$8OL|8uTg%*O`iX)OtUQ>cGRrbn8u}W-U9De->x0QhjTk0TH56U#S zphz#Qd3up|I;|8Me^9$2@Is(A>)Tw1od{ESk)YhS3R@e}>@B1Ow_FRGUhb+zP6rq! z42teB7EEvWDlE*ByeVpmn+JIs&HHk404#|gK?xDDj61g9JnNI1PwZ*49VUhR@-=NJ zXm|xAq%b1D(ZuAN1=F>*u5&0a#)>ahw=2sx3;m== wUnZTC1BMvW$GI zHMCniJ?%l>#qB}-C~gePo-$e;fY?7l0ui^%!M`+wnrSWeSe;yNe-?LO;IMcsa#EiR zxcbZq{~DLV{}V2MHyHlt7W^*ueiv+iF1Y@qND?wI&=>kg6GN2d_tEhRvR2C?@E%fm zuYm~CL $V#3!tPCx{Nt!exz&h8h4BM^t zQ(-D-# c*g!~<%Z~O0KhauW-=5pC>G}xhuDVUQkA>!DDa5WUwX dJ!&>BD#hOqyox`7HXLj0=2Xeum85H_OtT2wWPp)EP1Tw#bBGPwVG zmMjq@1(TVHOKSqzKDRXa!ZgIe23M@3wUo1s6t(BBJ=03(JVhl(ygVTrgGx@W%U6cM zKDMga{x0Pz1n*g1PHY!-_|tl~qDhdOrN?iFd#gmrT3;Z3uFc51zsvwo3>WQG_qJ $#QqT6mvMr^(GJ*heiicJ&|k{?ajkRMNDros3OgTQlSFs*n{j}pSTlE;$` zkx4G*(1fHb@1IFS=?uOa91$)mot0F|8ly_WUrphG$HjNYghCKtJvjt}y13;*anf<- zW-9sa*%(AIxtt3 ?chs2zo{S1A0orhkr_j^EY`qe0iX53~;V7*zl_W)p}GykFoK z)L&GtD49zzaV<%#oj|0sYY(iOP$0Q#VfbZZBT hD)-XCieZg+ts*YcT)b%c$aFOgEv5%I9i?4T258Q(rE?r4VX2-ddy?Xz?F zkXm6%onXGeirgPTl&d=UqKmO4u+Oek`ZiE1NZw`xWPLw+8CObS+Vk~wd=@VnapaZ& z)Vwros(3@hwHg>*Ndr49m#V2PwQF75Uu{lZ;z=#fJl z gJ)o zugCX@EI8$|?y|`&+m_l3yoh1bWN}zdqyEmd@?>DIN1kjYP#`S#KG^|E=y_1KvV2`O z&fEkFkY_&9Ap)15IiVb4R%p^QS4r#L;LTmt9Gq|rxkIN_H1%l(wlt>PO)iZYt6)V1 z+cF6SdkNWI?CM4K+|Xh;6y`WXM&v&lU+#76xiS;TDaQKz#eeCQ$Q@?Ts-8Z4>?XQV zCJKXcqBENCSr34xowO>Or7*9Vx&B&Ct(0$}oUU>bfGVr?Nq+g-DY3ZTdj_l4F)!b2 zwxi*kJ#qR1pvEx9;s_Ok4Oi*4Jcr<@K8WJ{;&G}iq8pz(LoM}aNy25i<15kg19ku$ z?rE2949+so@oqrdqC*QdwzhF9I`;Vq@HYU&`bw|D0098#zWf;ge{JoF{~MX+9{_OH zGqC>M{QUz2zvMNnmU-aaSr9*T`jB|b!*jUnN)!^|=O~yFNy4w4)6ia!U&h-w7P2Tl z+T8l#ucvPF!M)vCIXF0KU`cxbCA15U+fZF}wrtsd!kG|mwQQ;-w^ga2 _+kJ_5$QdsMR5&jI0%(1X zKqvOVvQJc}qGF x_!87iWKzf9g(RX`WCw4>}@TVFi;KY*15cjx+i!~y#?@m zFl^j-&ouzc7D2{Xto?h0U*@o-esI56e`fe3)4d^F8Hqufg^1OVnhb_YdGvXi?XWs^ zSyU68kcGtvlOty`ol>ZzUZ#rEtmsW9@E|9N4st$xA>kupzRj@{Rm0Wu$+aO&!;~os z@riRrw=GDGfh|hKjsc(a0z9$PCACz!I}bwy##Z^fB`6|AT5%FKmIOj(&|--%nLHty zdY1`%GAq?*0;ipg3Gl&iAPx&%i0UodAr~a3s##kKosvpLf(6rmYRk *-C&fdV%;FIp3N=Y%2 zH=>&kT5v&N7`xSvW*XQF9x l_}zijG9?KiW5DNHzTaE>()FL==F7v>fl6>Qq}i*bMH2pk|{(5`EE?$ z=CB!qM~0rWBaJvgdJ5#q;1#ZVYPLtSLE!FH;KL&yZHvt7g0K~cOnWqX9ah5WNh5ym z6r?6Jr3m&!ET9&r>(cS+XD5mKxyJ>*(iSDS-*oR?=Q(cvydI>Vl>aYb`B%Ej|F5su zADwbuT|0YQ9X $AzG!1{>M^EM6z)f@*ky6v7JSv%=(b z-Iyc^b4>LP>eEHbYZO(!X{9BMZYlxo1Q%y3=j`Ru6FR4F6;(utRXwPt=8spP8@W!7 z42t5& garMFc-48B-$oa-A`|FRjv5be$l1w#vI{!N^;kWAnt2bP>?*(0H~6U;7wSWG82u z(IG}>d(SrJleXuqoxfor?u(GhSp+n}&G)1pIWwe6Yr (;C zsSYdkvKoO!K`sj6hohS$<07;EFbB=rTgFP_3yig~K-P*cAlB%Mo!@}QyGrs}H-?>Y zf%Cu|E4=aMf1=Z9ZPK9G>^VHQWKx$PwQFqiy*fL+v!`!MpPFF~GsN9Gv%hx*5b2gB zXr+zQ07lqTQW$OZvFk+*uDi{%_a@i6iBlqFAzr~V-^Gc~66EzZ9jIH=ltg9>jJ?5T z9c^r{NKjH9zkEG?4HNhn0h&H1X+{p_4hhSvwTorOSVo9fiAZEg!jMCMj6GBBHehH= zZ#>Rg9H{^kluc=p9dwKY1xe9&abis1(_W@;V&atUXP9^^I>+$nxpj;vFf%rlp(j*a zSac&6rR}LgngyN*mKL>@oO3u^nHlk9>v@y9CmVGdYs_X4@E(@Ug`redIO$YUNTVY5 zOOquI@7D88q@-SzWrvY$zTA7IqCnnY^i{9{orf`0WGI~=4~hU(#6b7Ou;6?fgmojq zF5YPuM|2?QL1AK9DCb *}d^jMP?*l&y=k2S|*`X+bGxis`;f@<>ZaF{Fr|UevCbzFY-hf7f42O}prn z#zw~$;i12Jl+nrQSJPk?*u)CQOImZ0_Npj(($YPJ{>vMH{pmDyFAOViSpo|d9|`qs z!g1f7Jwg(hpp=DRgW_DW=da!K-pzO@z7~XwLjpYDa?`i26WO1~jKZnV6{HKfdt=)( zw%koGOwT_V)V~?C!{gA(tjzM^?%?G2Ez91&AKro%=I&H;tLOy>OdP5O9X>xQW|PNT zC-%pHw_ZZ?IqV6mWNQ@Gr`z|)`GGjv1(Irk8-*=Wl?p1f!(XZgTqp5&AuFcb #rhxeGws=Uu~CX@|k(Iv?BoB!&oD2 z7sEKtQHB;?B4BGNdz?1`n`~ypRMDXygDTiN>iUwpQ{_;?3*3VWvo-M6MV?DLSnz!y zJwP)({&*Wi3Px-hJ?#=+5&NQNzTL3jQm)#-Rmf5>Fq{1%lBP>aW*Fsr$cY>0R;Le~ zgQR3i`ma3~tCG{BY)fu?g`oaIt*n%ViwUZca*m|ljVp@D3#tbz!0!ok70k96>UI^) zI2>*dsB$^30r5jGN|j|F`QhEzZHoZ|5BJpgFE-DdAn!fQ*F)_3kQjpJnHV2`B& Hfhux}Zp$Sl=oSmc z&_qC`&&Fw~n=Dm~D<&Z&?++RrUVIDjf$rZ9GTg?mn75240}&(?>gEVGRK9U=y#a|* zZzX<8u3uFuDfGQDV6aD1@}H7RB0GWHv=Y;=NH@*0g;wt|d>Nd0i|U&{64=*sZkX>s zZ?M!A`G@N9;{BKLqQQZa$>1-O^$d7LHcQGNyNHC1{nj3ZI522I&z*=&Eq-e*h1& z=+0B##utmSU+n=pGfaRtIl%chmBw8pY5lP$s _eVtJX`b-J!IbB(7PYV zf@7 _a7u8Yy2fWK0~Mf#xi6y@nYn>+wK-YA(C{bTCAU zA4N&^5F|mu1PpT_3@EnI5K`y*nl)JX%F;!|$v%({ MuJpRPd%iOZ#Rk0&^Yp#!Ev* z }F2UAOV_haJFreYK{+6HX$mnfaS0C}ic?_N1E=wqV%$wR5p z8N U?t)xKWeu5PFP}Bd# z>Chov(Bv7{2>H>b^ugliW+_Mt%>!oS%Z@eK@uG_y0mw~z_!YfmgTLJ)`{4)3Z`~qe zh|<*jsq|bx{*E{b{~mEfbnJ{Jb*z8$N7T~L>JRe#AzacG{~=r)KZQ%^X&)@-r*NsQ z)!<}R_0MO4=b(OOn!K(X4IuawE+@Z*OJd9qQAZhV(X7Q*T+V}4ep7oF570z9`lt0_ z`<<=1xj8F3S|<0XU}nn;PD4xZDk*|-=%oh+-ZN_uf6Z)=4&JW^1lgX7`rx=z>?TaD zA>~;U5NL{ry4KjPr;>oli@q#4kb$!T>?-2^URp?_Tp}>RL$<{JQt*z`R^391sQ{qd z%95kgP{U{n(K8JWj7Wap0S+?5AD%99rz{&n1pu(oQzI0TPlXx)8jRd?ZL;(-lSBZe=1G8P$v3Yi7M|9 zy^noCls0bsI)R^|)tR~D!igI|leyjZva|!l9gd6BKO^YQ5x4{_VL0P-MUWwRw1OR0 zn7MV~{A>v>(mf^g$lNgM?InhaqMZL*wglCY^EP22H6=h)&6wcJ`OTV7nG%4oElaTD zR1gN0P6`i6xNh| lY2^|;_Js~yZE(w)SMz5TpKn%rCVQ`K02!%!cu=Nna4ZFJ( zG#4b2WD=(IkJ(VUKXFvZjl{t{=P310q)7RiPX{q6<-Y;d@Pv0QpKY7$DV&&(q REU+!VLPhPM5ja9KcSBk=}3(jrWtwNpflro0D>Dm z9$;^83@#I`{Xo2hr$$7$mEc}m!1d!xVMspIFUB3tMjxyZM? &2J-@AR9Jfp|Z%QgjSOBD$|+oaD%Lqg0T~YdKRvTSqI$7X<6`h!H6Oc6z!;h>x3R z9R@dox~;$$6iB44tq#lKn~#chMFSBKi3hjhk*O@o77XQB%7Zz+O}m3jYIQx_)MV&+ z$ISeCk51{fCyi!yy-ieaLc#IgOLR~|fvP`>pGl4SXr)Gsp)+ Lyg{2 ye_4J_nipr zDjS=A{MHAhw;QjIPGPlRA*D3Wh13XxdFX*!^%tqp-Kf t56 z{|H-kY8Ic9M~Lrbqr2ZXZ?~k*2jf!noMuSI6%=<< EZ#9G}pIvRYH!MiWF_j+@mQ8*LUl#GE$WyPe1=rIzF!Q zbPjt5L W`DSYMHR!eL%YrGpY+ zd tC`1cS{0_>t^Og6hD~_?(5x>@4D_1menK!Ics%DB2OsV>dz=d IIRDZ<^CiJb7sl+w#LgkC& z^aRDdmaE!$qK1p%e9Ts>05RuDLWx%^lDk|xiI8z-E(8Gb=c2{MT54@Y#hRZ)9P)!4 z$9Lt{)Ed2}LXw@pA_Km7$;81@EGB|^PetyE{DA%b$TIKq2J8E?7px1&Bdx39@{SO= zHgMwh;_27mY-0G~bG;R^Gtw`YW_ye8G#Lq8PhzASsoQ5%d+^Oss`*~G@D1=8W5z%* zLzYP6 6~#$i^2-c36Yc2jE*$)9%v%*OFNO_6*t|g6I}Lb@T~~c=!)Vneh}L4 zTychKJJ&x&Sl}rwCg{%EL2=XaaP<4@yH#ppQk4RwS7e7ytH(u+X=qVf-vJp0Ais@s zQu!yKOOR0c6e(cP6Wl_zf3;5&ya`-lFRVC?OSfzKIT6bEU;}`2jT2^X&}niEvL#^V zqxc#XCr2ND>KDJWgk5Gq44BlF{AH4lyA~fnxt6dE36N)B5u%Sob*J94Nq-7;QyRV( z<`{ksHN9HmG`S0S0EE!$O4vIcNbqoNfMJdj CPLY8Kk?_2rpX@!6PP2bZ~Z z+G+@q{8L0?rU(aZE;VQDx3Q6-p$=;d*bA#i(BlJ8?gUoKs=WpX_pB%ROAWP^S7mbZ z(f#;kJ{&EQDN@I1V3@6BYQ;H7(^k>`b0L07f8+qp)N%kA=e$)XPfkykC>3}@aCPYK zbSXa@n;RRGKR7#n@ZGw v0SUQ&EZ}zl`A-r1v_?+(2vHKm6LH0OT4hf#I`w2j8d^XND6@N|m?K!G3gy z5j}lO47=>55_-?Jx$J|uAmZB_YP#Z043t$#4u5p|trXy~Q|br4(L!jSK4h^rd~Gq3 z5U~5NVdwyE6%-EW9h}4a3m&~N)H>>$HhrBGzWV$bTlq79(39av?T~SyQm?HX{|a@& z3QnEzgc>-BIAU{896?3$pEi-oDrdEP3UF(^)Y7qyT>Yf9t~mC^9J1uw_wB4n7hT_I z`uW)0dI9yEy!AF-Z59|pu;?ZdARS%Bq!{U$vzCf(u~Mv=^62Q+H O zpT1wJkk5<1T+PzZ4MIgTGmvEc@|TqP7;C7(w?9ccRD0W-1tUlVW4eom_QRDcGSirw z2tFT_)!SK|DLunrssO&3QsD%6#tXWP_;HU3@3l_l`9jyhmI|CjATp6{V~U)7)u=zf ziz 4>AXZvN6JVKKZ zk9AhA=exU4FD X1?jDlHUfpI)u1&(Mq0JgS0OEK0`Ec3s|kKC)8V;5b()TBe8iJ znZi9B$W2h0RF?B38BQ$_aL+Of)*?HlNuV?%Od3u4HE`EPbC>p}g!6;|q{A3JrULI4 z>utp#qi%Q=9S}s7*qb|OWLN*ZlsO#_rnf9(ZXH(J!I785(W#4 Q;~hH z<59zvplkO_qlSqh6N^j-Xh|*1%q><%TsmaRm)_foWW`MEd1xew+mYqjR?lsTgyXx> zJd}mZ!@%PC@>04-^JbY%gw_`B=bEI#VU81v0~Mgkil f9oM+3W%}E)Q755c z*%`(ME^Xw+Q6Vl;4rn%|9UJhbZzy(Ddr3OBOs?>Xk g~#%KQLM2y~Fpus)* z R D*=FazqB^xe $4 zJtnKMz&(rY@S|NMI;*FxnNDklKTH8eu4D~|2vc^>`(bc3S?{`*bxaKd+k;c%`@?n= z_G*E2@>)2labN{g?cE>xvI5^Lz11zuFX@p^7|2ViTD%&UyoKb+m~7o32?wg7b2E?) zaF!{=5z7&?ls1D0sCvkq^st^c>tJlpD;g}*0rN0=F|3UPo|E;5?h9>X#;pBgcweqr z`N9#Y93{I3^Q?LzfCA^+ccJ~QfU1D0(b`+iwHpoyk?dod7JNg1J7 jnLX`d~^(x0j0JRSi+|#JF;jlZhHC*1q-amK_bLK|tQj+NB9D^2-%!0ht zi9U4{HAFwZ$6F^6&@f-P?3hVAcgO|fA<*tRW>o@5n{u6h8_99N`^c{}+NXWMolA!i zvNeNDVuEmCe)sQ8D%(l*^j(JMbVan^Mq$r_BYTK{0&V~ZPZhC&8jtIoVrXxo#fZ7( zOxp5EW}StCqTG6$sineP?xd^(;X0}E#_h{3#s<2TRwg3Rbf&igEP5kGprnyytTVCA zr$Jl&Tuwx{Pvk}8?Ougi;stBWlW!{EW_Qum6py%D>-m;ibT48 uiqi28 zO%TbLJ~sK8(3rFnQ;nhjc{YmA;_@)H_Cp_Gig_4zT2l3E5z?>sG(%}_9^??YweWzg z=SPN#-Yd7$u7Dk>jp91jSPt0;n$xCCqQq2ju{hV>fcZ9375d(;fVUKr)AMfBEnFP{ zygH_vZ|BDE;N1iA$e{Tu%bOfO>v&FMk3l`2=??&&x%|I0=D!F3Pz396 9$7leCwYLa6Gys4!>7Of?zb+L?|9h2-oPnLSm8G45 zh>oSc`9D|K;uS7Fmx iW;HboNN# zPZXT>c{WOr>w2Iu(z8L9@kULPucgtFJFSe<889+Jp}x9wRzkrHBzo%^VHEAc*5Vsu zF$CL2l_CP-%9TOH1 m=K*2bd(OBZ0N)>$5E+qX%bf6#jck}^XZLG%v6d!&5MB0*5$ZqJQv zTY}&kj_jGYhg2Y`P;Hsgdtz2>_oaQjLDxJ9KQd=@qyt=UHo|ky&OdbW#p6;Ko_#^; z1x9Ex5p2Sck1vgDnGqy}^6OsBom)koHKyEaZHpi_^e$A0ClK9Qmo ?55nMX5-p|(t~)9dXO_FvT0~-V^GsO-bxI-3Q!=YLVd(r zAL_he+H51*HslLGEMrXW32}@{h&kwGv8*h^=U1&CajH>kJ3z6nyK=j3%rzV 4c>B?{jFg`)LhrJ+?Tsgrx3~jd*XI~esR6up4R!DN=O%A=IZ}Wb=_wYUK zJ7)EyH)-{;CgQz>23nLy`WjdPm%w9euaOzktx6yh_D{r2j}E_y%!&Rj0oVw8M@T46 zC06!J?_a011KAtif~JkHtnwXR{*joHMJ9EnHcYVW? GIICiPR+&9i *zzuWb=cy2m5E(M~ik%2K8q zQ3Oueq|T5Z7Ep0WSe?qNpS(eVx`KhNk^6-_To$9CC_=H*U?=Fn3u-PnDF)iL5^3#T zTX0J(y{~)5f~j2Fo#SOA`ZG&%xw5P~b>ez_K>cn- 7-s*BA082X|brfT1Uqf$OWq ~bXleC@X!n`di`mhS zn+5Ae%?cz09@1s10L26Iz@6KD%b2w{z|EFQFvY4m{YNuOnB@#B5hk95P_|1YUCHGr zm9PD*g v40@bsVJ z#@~~ Cfb &uN46aJP+pS2f8Hn68edaOI_uJa~wmdhY#4l``bNa_&2{7y9F z;)> I0MsKlB2&8HaP|W%2c1CT{3!4#G zPSXX+!ylgI){8KuJRF1rcYTYEEm*Z-jOz(C0u6@vcn}UH0|;6>bh}bb8@aBZOAgKy z=lU&n*+bv$O^u~}V*2p8vid=A3rrq+Hk9#VSl}Q&VbUazh(W)Hu-{ep@WR-Q-o0W( zN`k(9gXp?7taB%PI4t`;O{7v_k^XSvnZIGBk9UO20H>lk)NOp9O-e7tf)g*!hS0@C zg3vtR+H53lJs>u_a~*$VEELEkLC-Zv4JV$TFsx_?&qI|uJJ`{q18vcGI6Yw7rKyMi z2{Dy> h8MQ+ENdK z0?9Pct<_Ku6kqYp7UN5}oknBeHLwBoPss*s;0dygo|LYe_Wfhd7%MOkzLKXsJ>d+uVG#k+`-X7|rh+(Izb743rgPr% -&Ryavd%~zTMYge}CZ)wo9!ncmFQ-_qV^jJSn0TFF> z!7uE3=;39C^0v$L>4a8+qK^^d#^|GqCCJ#BX?wAB$}c9#AR%p;#7)@R67a@b( 4j z{PNl}DN89!JLb<{LR&(0(wIMBvl^ME2^4GgCZo#FN6jFpnK8s9=sWDKeqh|Pjx`rT zf|O8_fDw;!vL!Re%1RX+c=7TCRw0}FZ7yUu{e_NsH&&4eMxoeRPp&0yuDMDk z XK1)z6egls^9mNR*_=z-jeu#Qh4d)Q_q` z1IS5j+!$31rI(3s_VpRmFL{M#gx^83ZanJiOoKI0@gNmxvt}ImmofyyqJ7J&tzBpF zL|UWymdr;AY@hq1H28G=bZQOUb!gK{^=k*|ICM5Z|I)F6ERMwmvKI(!s7l|>F6D+f z!C!RIG(B1JGs5eO0tfWCxKkuT`#;L^ifYwp9dx$XNC+}lvj|Pkg!5_hnqk<-EQ*z& zioMy-0Iz3(N*oD`Ma4i_< )$p>~L0xQ_qAoB|IHD@FF5=C9Ml+^>DljUOl+sO- zYF#c|7dSdy%B0ApC-yA?;9}@cpHHHhT9k)dv{MA|)29lmqQ-$Hm$8Gaoo5kuUVIO6 zcQaQytDoW8TPA_Zmr!|k4sg?KtqKLL`o3Ky(a<*gT5Da UZ`8$?HMg*(0m8 %<9tH z#pH5|Nfk8BZ(zUWSjy1?6V&H&w$R_sf=K?a<=CGiv7CX9{y(d$e5HRx;m_&{j`bpR z)zDVyRK5oK1~DIHpWT1AmTWmEi+I?OFcw3!?<(N+VNI4cLB7ytGOLd0S~H`?&3Ry} zrU%#=)Tl(oj9yE{*~a}9vp3(5m0)VPEPPMg?n}0WdyM_GUbMA|Gi1I*6KKmO9fgWd zr0W6rx{ZZj%w_j?(8w>(cS@et+va785aRWq#Z++txd=LVNNivpsh8b8QBg$((NH8v zu#`Gz |=6r zl+!6gt(msWFYgMzasr`qk>K-0=KuPzRUwPjFFZc{V*UJlZ3=lIEX$N4(SsK-0zOVd zda%XRnzp$B5=Mf%d13wO3kxYQ;?W|T9P|qXSuy7?hOc(*AO!L#Wb&5+`UG8vyRG_m zuyzhTb-h_xo37;0@e6ppvr9vLpOYbL%dFXTqv$v|SUB3#6+?JR3#|FKV`>FSV$*kw zmQ@7HtkAr+z6!jxV(O2>mPxctcNIBT5URxsPMBSX#$AzD2S6R&vBJm{6-2JGZ5O|` zGP+pcB{tv5Ms_s`}mPaz@#{Uop~gu>zAWr;^9pai$$(Na5=>W&gN$B>;}hdrcc@ z(n!mAR~vaK(_FCPlE@tyiXWF%y++v#k$-W1b+ML%_$hB%IIRSW;)F7rX@@Bm27p=6 z(1WaMLtD3FReEgEgq{}VJuIPT&i@e5g+C7!>3Jus?jcexsQAW$rDxJC-eXn DGUu4 $2Aeh2?;DWfdX5pmrA~4L%=FHm0C#2@#U0iE+J?b$df7 zMLE=?BeXr%xLApLT(V>l>(>jsq52Z#o?QO7VCUtO;uF9Cvr|&-VV~wCe6QlH{k|X6 zSM$}&CFUC_znJZaV?kh9M-?=<6P8>p9oS^fxFo(K=EgV~E9ZxGXP fXg(r#XDx_Fs>6ey_N^a@;#(ojo1JilDP{&z1QnGV*zDgSrrVpsONn>e$ zveU5m ^ML%6$Y=OM1(>idAyistOwPe{YDw74CvQ?gW0%GThdD4$jS z65DL4b_Oe4H%$Ycj-T~_rT2D`l!|EM-|vbhQA141O)dVKrpQ^iU%~z= x zqw_H{ r~S4!^vRC&v)rkfgjSl!?M7{RW!*U medvq>|Hg^|LaqUkx0(lwA7!0s0EY4pCh% z40p&^EJo{0kQRAkI(nBP4{m-(61~I{K_c3E#2bd@wL6tx7nDA)NpsnP#s J z9)j;2e>H%GPHy+|${S~*0 xNW%lt$$D!x!P*OL;d35d(X%F>ah_s z|DBl6Kgq8ea5W{M$sH$alf&$^68I98R+jic5uOJ7aig7Ns hEV|D bFFjPZTrJAK*0HF+)!$C_39r+nQq8?U!2XW= z7px1h53J6xWeJbK &Po@v`$M_;|K4?y@?G9HuBlz^L6hU=(`# zRkPOCAS{kj^mC^`w5>RUJhQkbCS+r1VJ8?X FpjF* z=uhhXt;J>8>!@j5=a!cIQ^C`O!y@@?y&Z~ylBI5UiufjoD~SZMt1y&!S|!e|7Dp9{ z;*0mxN*XCHUb9K$&IQU}5MhWcwU%-BW;N1iY8O=j@@WBp9;X7dy jblwKBox?L5_x;cUan9!h7ELHJhiZ1{?_qwNu|Ta5^Zr zmiUd6_H2*vHO?gU7kar9)(F#25>!9vD7x;Ce~u!NKge^6M>}aJfvr^1yKU>yeE~Ga z>5|c A|If z`D;MVyIIYc2?m@OLogl=Gk?JE-Fx5nfS8+k;|&3m%q!I_*~dOfV;u$87+ddP4=9}B zGHMlgyz?f+{_~o6z-m7x7JY|IDV{iAYWEM@6%!AvaBJPrsQDL1ju*o%1 z^rpUzA14n}dZL3J?B!?h@orM%E`5A_jfCgk!8*Nrb)uVC=E8I&*Ne`Dh_}KLFE`vq z0}rT{yRyRDFVo)<+$|%#R;>}wP%|p&?xF4x$?TvD^oZ8*cYQioxpcv40_kE%*dyV Ux#ja@ z3id8^wYS;|vef2HH FyECG+A*v^!b^Q zG eI8Ols@J@;*Bo zwT`-Jnb5uc@cw9NeUC^{Yu5_l>(7k3qD$w=?7S=~l-tB}$L&9AjCo3&6!C2Z5u3;l zUmEAGwV~_v^|b3;N?Vf`DO>tdm|~L7xVSL(XysmP*mLez^%mWV%8fv#3wx{sO-3Qt zXZtJngoCrGpLStyehQ*j!^};tm>o@fso+I#>u{gf64;jnBBzq(?-qNIc;G&nTYk+{ z31f3Q;Km%S)^p?M?jLM4s-CDtmaXT{sVjh}E|7E@J1`Cj^qa`WW-)~Od30W9u@jcC zo@3V?KdXnygP*Lmpz78}xlCJ g3Iw8)$dw{W&!|-?-Xe#upXAyTmBxuK1;LTJZfYI5^basEJSS2I`MqVju z@E9bd^+_sY4NpcIYb6|DUlEN*RfxIIi6;mJ*T^$=oNrMD@#3jE`V_@WuTMMae&WE3 z{Y2o2bEUfv4LpjcJV8xsrC;|j=iD?N9_0G@Fdij!jOxccTA?7~Rq4bFoR5ibK;oWy zpQdRfaJRp{onwsAg~OClW S=b9iI zj1^oOO7{{~!fWxaw%w+|Fk_kvIk7!EoGD>R!@jaN-M-LDJ@M?6znQl~t? 8`qMMI66>@{U<%Ty?6D=9gHa7gSOsfC{P-%T5 zpQa&@TMcxc{5>lrq;Dpw$=G@h+RCV=7+XT$Bj06l_H0<2gvA}us4Arn>!6NeKwr!h zImNJ)Pn^`}@M$JDm}I@3b!!8G<|P{a{(;AwYEv5dn7QTK _-_D{}zO&gQ!f@bYw>`H9jl&X?R2;4grrRyIKNIO6K`anP23Fxi% zZc&R)2gvQiv|E`}cJLIN_(h`NoUezcfZxs%7cWqyk>{Ecol3(<|FD!TFe>YL>R#AG znlXh7vn$%R0x?Pz^ lCMs}G*H%F0!lZw-s?o)8 z#WbCwhxDEKZhbFVZ6Qf^7#Y^d18=T=!i=8R9hoOCW{4s1q`6sp79*I};t}kAEfr_f z8$$q~JIy+Z&AhV72B*0ko=0H*Y|e6x_QW=a=)!wSQHJT+7S9r*utLF$Y747raazVU zxB_hqLUTS^ACxsh`Y<5c{Px*$u1&p{pUiJ`B|N(#16&FSsd&-5SvjFD1ax05AUiN$ zgUD+HfHi{#TD5DfJctuz4hYCk^qh_}0i6_~5a@Pm0I21j#ivY;DBv}b8@aBuOi#hY zesTgLZpEd?T&9wxA&?1;$!Ac#1;{F Xpq>$%@kx@zrIa2i?GyK8&n0HE^*NnR>Bzd(crnDo@N9r} z*`6Yig*(N`FMS`aX@ABAuwD+sLEE9JBO KGAqx ;Q=96}d(X@h@5wChNZYwCiE64Ftqe{WesTCRmA 7R@ngHk}a)+@OuR1C34=!RWYi!gySM##^05sF0cVT%RYZFg}tz>2WvF$OKG;>N` zKcQ5{@&*FpeXQzB_^fUh2zWeVCZdDN8sG-^8saIZXVzaN@uQf&YoWFK=~M}ce1qJj z2ev11M$(d;moE>rU27d+h{7}r1^9p!$#KfxY3mS^ma{*X zZ^!A%gzG6;)8TfA)7b^5mGt|wW-o| fd;(Tt@_N40mal=Io|?Z&Ug=!@d%Z9FFHih zWjzo05-#j54 H^hU5>Yo6~MDDLo%5KF)8NNV@#|4IMWj|h|}^r zcAfKd?@wIrnL?NLob2wgmYN2c5AdZiJZ GWy=! z7hiAAt$&OE>5vb{c?{oJnfxXyhJJ$l h|!j=A+Em=J^%hV>MkNe_WaNg zk8{Ise|r2-1pRmj#8%~@As~$5x4(q=CUd5$@Er#=tqP%reP{@rIQZ@HIEUKU$73-b zL49kLf&YkgsFi&@9zreq&=6gzItqzHF#cZpigUeFX>^_3zlm|1 O{avMgbUdr$xV{Ywzr*>_@lt<_ zgX(yw4gI6zP5wtLRL4Wb`_b{v!efDc*`CJ^eZQ;skB(>cKVu)gtKV__=y Obt)s{HdY-6?s1_@OK71kKjHi<;){xdEKtgH)DPfS zC^#4%Ht-Y9&+@0JfXK>{2wl@7gMznz2M9m&K}ABAjzcI>9vKuI3g1ilsc3(uONmN~ zELZj;=??jylcH$UA(J9{53 Q^=Eg?`1%HG)^Plm4>u$#>L!lM`S?&Q5i+E&EusOqD6#ywk z8c7=kl^!{18=)z2WKeK2yc7OK{f||cKrR8|%_ApYAdb3@3<}m|{kszUv=#Yn8xolx z;Q`38;)oQFBZGp;1^ *O5WNcP0Nn=#j#}P^pn41Q4+YM+OC31Aa&S&xnl_iGYfXe4dMl zFgP+O7 2IS@3k00q_oxdsrN*6`6#XsE?6TWgpLpu-u3BZF_KHNn7 F`#=9wXf*%; literal 0 HcmV?d00001 diff --git a/src/site/resources/Releases/rome-fetcher-0.3.tar.gz b/src/site/resources/Releases/rome-fetcher-0.3.tar.gz new file mode 100644 index 0000000000000000000000000000000000000000..958b25e45bb9d40778f3c21a9448d704ee53c670 GIT binary patch literal 176706 zcmZ^qWpEqqwx!L?%y!H%Gsn!#%*@P=DQ0GfnPX;VW{#Qdn3-A9 ?_MkINn#uTAM>+6|-4lcm7%LM5e%-y71lE*<8x#Z(>DoA>vW zoforRCnUs?^3kV7U|e9brCE6(AUS&jWzeuF2H}LTmL5jBJ~=ssnbOqv5 }Ao>oRvA&Oc z2>1G1;4ZHKub|}LGZ_;=`|3?sJsk?{UrqL(cF%~4fm$;y0>G!Sbq-QY?eBm#r43q; zmTOHUNMGLg?rHa%;~${W#NRU b(#j{_pL23H7pPRE&9C~$AevA!3)C9DD zErA>tTwE0UNA7lcc_rWKlIfvABRqk?=2$=*Vjmf>s-5k&uk+zVN$xFB82n!k+z+yy z%@<+}0j2ygYX)h!GDd<%a)knQB|$>=`Q$+Qj`ZQcfIp!8^hQuyf- l4YJ&;NZ2N k%cKDPnP!PnXOt#mnyXZCV?VBq25 z0qEwn+f0H2!wt)wapD)N7>|NlR%Y`Xki>5L6yWXg?BwTXbo#)36?}cw(5Ex>d13(? z^j8chglXZ3h#v c t<*F@Sck>JQtOm#hya|xy4h>O|XA$Zn z*D~*C0NrJ0p_h|$2@9Mu2lfcxXd`pLl2U+eRou5D!7mf*Ps F`B_9a9g~!}Gf0oPpWh62CO=jfyJl#7_hwF?KabBh!2$^9;=Vu ICK0ncR!N=2dA^Cy-r6&{ud{th5*!y$(w`Aq16}=IcWed$QST*?0?}? z#NP~Eke#)0>L2s+!A`n8R%Zk=yaEcE1#UPdJzM~ jF+? zhIsj3=lS`KjUJ@9Z<)b*K>P2>-(P**T;Ov_TF2|Oa6^=C^y-g6gV}gnw}HFhU- zk9|+#FRs6|Q2epJ d17+K(b`)6I%Q%uHLP`DGk?ZpgqM*|VF=N?bp{ f(s?Xep%J~LF;$3H!t_C~9nS1bWP!%F9; WRSJpQq;W6KVU(cGbvV!P$0T z*>*C TUrSet+O&2+j=<6lNsqkB*DID}8j%re*2^*u2&)%Jcuv<>gJ?Y^IU-VD{tBMe1I z8SQw5RCM=-Ui^_VVgeR@^PjJF YN_GUamx?Q7K;*-jHaRgvGr z1qa%<6ENQoZ-m?W$x0OEf|HfB;w5ZIl#%v&=;pbL*ke%Ej~lt6_xa}LnLd>&@d_Mc zPjCD-+_S8#wJwk7BsmLzuanqw_`p2D@66z-sZ&0STi{hqoiG4Q4no&SI^N!^_Lh2@ zrsh8BUm_T+rmDYna*tSU-<7%gJ%PZ5b}v|V=wLpsj_+>{xNl}Nra<2!BcJ77l7c~z zmHUoCq`st1#=9NZ{a28KgOHEe@(Wjhvfl4hXEkq)u!IbkHn(D=Hv&Vi( g*L Pg z#rW|r+nJFi_4_jRheS+KaY)S{6;|8=+`r0}qsKLK=P_RCjUcY`95Ahlc%j_Ki(0o> zc;3Hp9v{AiW-#T?#Jw@hQL@PODh@$4;XB(-`c6 Uy-B(t?793au#{_9px!Vjs@Zw*J4v9WnCmI z^`P*GImzFsJpROpnQ6OJ5NlDW4>N_{_dvmzO3sXYK`8kU8(lu?Wr~~DaXNK<(G}z< zSdvn`1K)Z*xIewh=pt{bGPZbzNwugvp`AaN6|l(ZxVApm*7m*g>#(bv6JPw>n)#nN zye}J0^8q2xgw4AJ0GKOo>pqbW1;>w>4qm_gW<{VsTpy4jC#lUX &Lk8e|7Q8tes z5**qVViV5419h%}l>&h5>%%$mw?@N@Fya-vvkj1ZZWOQXMoS-7;&{`wJe+l9zpu#8 z{c$ft%Q(Zwu-EK-dH>Wa$M~{f7sZAgT-`L4XJ3Y3E1Da_`?6)ivHthMj1NC>u*qz? zl`-T^UgD$^c+N=4ty=h-$jqVrt;U6hxOfK+<$_=cWGw3fVMu5K+IiuIt9XZQR|AQ6 zkeCL0iQpmtlQuxk@uA8S$bTgc=U& zEh@{kQp<|8W)bI}O-olr2jD-7LO1a-(bj!6S_oD-xs3VgaO|as#8B8qyu(LF*qu?1 zg96+DWy>k~ zZw*NI{HE#llDjoF4d{s6hHQd}S_Kq?k6#@12F{t{Ofxq%k1QlsbfnX8&5Pt%RU`_N zu(fS;uaa9%D$|O5EPtX9vn7m>g9l-ox(&t@80n_l o{V2 zJhy9$M|WPf-f}5(L9v;eMdFl;h*zdt?Bd %LWq&GJWRP*hb85XT%a1pS} zn~kfouN+wP9&_&W?c8ZlW~dy%CHKL2%utRt+Qxyz(fBeN^h2Zo4@%chUX3fs6wdmO zsIVNZjRta4WpllTPGR20c-MfOv^UG7TJEIZi$Yb#{3LlxZMPGYcA4R37!1Lq6h%=I zZ4w!}_Tz$igVpB_cS;(xUNV}ht}OxU)@|YF8Ms*#Ve^eCrAukja67J5lgaXz#Hvwh zYGQ^CgH1Q^^YyaD0nO`qJlrfE(Z`rVBP4+7DXv%_-uuWrb9ex2EP}&Q%lA*KX`YNV zDc?`4HbmOE;Xsm>F5*~F^?~m0>s*`6T|S%1G0Ms3ci!8jjNQ|zCYw0>wc4O!g8=-J zO~s>YPci?Cygzw!0cSxAs>Koy5Qrf{+)S@cE(DI8=MJp4rt18RFc7Ya5|4PV?!uwV zE3WFfmghAyn!1GL4UYMpl~7`3QF^*Rx?z6wp#)zA0qG_8&P3rSEq72HkbO(IZXUvI zi~Gf4vS`a!_Ge`^n`0Ili$ANt={%9&J$_EM^l^#O(}%y;5 oGFuQ%mp@CsxR>WwWmf8&<)^fo2HA9iJeAJ1jz_lNDFRS?FOUHL;~6B<(nPLTDU5 z#J&haHgX1Bn(i%}?jk*`lWE0jH5`=@qLQrlnz_r(GBGVNjg$hS7}H) dih34X88{a1(PZ=R}(Jv!@GM%9nA{xE|zTZAk3*}-CV7o2& zxD#qaisHH`A{ReO6bT^|T+~Eg+1Q>sFcMt_^X_KjO2mgA+{#enf5_uT?@Acc-DN z@tS3=b{N}zDG3hK&hE5#_{k~xlYMVYvtjbo&vd%=wX8OAxc&U#j7hX^wy@Kiz%>P` z(=+SXbpSSpVfHOCxvUc7<_JFD|J+qiTz~Hbb+&iA9>!o e zwFG@$rQd>F;^#z~n0q!}nV7IM02!y^bdt*`>EmrE 4WZA@9GpGZgn_)yijG g`L!ph6JKYd3H #44N8 ztkHa$4}C=Av_y(S@4NVOge|w^8!nhBUa@z4|16SSc BFeMwm!L_XdyQbqG`^MTe{V2cmPU}fukF*==Xv_}D>FO-JsPht@_&-I z%uSD)WDvgfEB#Smp68#p{M=%S>A$~}8{?l!6OZZ=K&$y}$8x9M8ZN#UcG3ct7fd&y zg+|p<3#!JZA`56ndTX7E@y2A!Q`)V{ Prqioq!w?YMrCI`CvFPJcqA`gXTx&u;! zo=Nu_ZZk~hKYex&bJt*g#k(#d^S<{qtL(F&m}>RjOXErgfJ{N39!?2D+5GSB`GI)d zWVM0NeS=f@5h p4(#N>|B^jd`{A?+peT5+}B4T)qM zZ ;dTgW9oQNf%8;T3h)k5>rRZp72bhd {-k$U?rsYXPMw4h2R@upWNcnUaD-1TVsoA(&^ z1pg6}Hq%sm`Q%qv{YQb>X_QuHSf#bSZDK(aL{L4!=CkSTOb2I)pbmC!2Xih|apKad z@st4SsKMi?&S_TK&82wh9(PS-EZ!6n)=h^FaSkOxhOq&0yJu8RTLYmSye>>m(q*`} z>G)Pw