/* * Copyright 2002-2016 the original author or authors. * * 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.springframework.integration.feed.inbound; import java.io.Reader; import java.net.URL; import java.util.Collections; import java.util.Comparator; import java.util.Date; import java.util.List; import java.util.Queue; import java.util.concurrent.ConcurrentLinkedQueue; import org.springframework.beans.factory.BeanFactory; import org.springframework.core.io.Resource; import org.springframework.integration.context.IntegrationContextUtils; import org.springframework.integration.context.IntegrationObjectSupport; import org.springframework.integration.core.MessageSource; import org.springframework.integration.metadata.MetadataStore; import org.springframework.integration.metadata.SimpleMetadataStore; import org.springframework.messaging.Message; import org.springframework.messaging.MessagingException; import org.springframework.util.Assert; import org.springframework.util.CollectionUtils; import org.springframework.util.StringUtils; import com.rometools.rome.feed.synd.SyndEntry; import com.rometools.rome.feed.synd.SyndFeed; import com.rometools.rome.io.SyndFeedInput; import com.rometools.rome.io.XmlReader; /** * This implementation of {@link MessageSource} will produce individual * {@link SyndEntry}s for a feed identified with the 'feedUrl' attribute. * * @author Josh Long * @author Mario Gray * @author Oleg Zhurakousky * @author Artem Bilan * @author Aaron Loes * * @since 2.0 */ public class FeedEntryMessageSource extends IntegrationObjectSupport implements MessageSource<SyndEntry> { private final URL feedUrl; private final Resource feedResource; private final String metadataKey; private final Queue<SyndEntry> entries = new ConcurrentLinkedQueue<>(); private final Object monitor = new Object(); private final Comparator<SyndEntry> syndEntryComparator = new SyndEntryPublishedDateComparator(); private final Object feedMonitor = new Object(); private volatile SyndFeedInput syndFeedInput = new SyndFeedInput(); private boolean syndFeedInputSet; private volatile MetadataStore metadataStore; private volatile long lastTime = -1; private volatile boolean initialized; /** * Creates a FeedEntryMessageSource that will use a HttpURLFeedFetcher to read feeds from the given URL. * If the feed URL has a protocol other than http*, consider providing a custom implementation of the * {@link Resource} via the alternate constructor. * @param feedUrl The URL. * @param metadataKey The metadata key. */ public FeedEntryMessageSource(URL feedUrl, String metadataKey) { Assert.notNull(feedUrl, "'feedUrl' must not be null"); Assert.notNull(metadataKey, "'metadataKey' must not be null"); this.feedUrl = feedUrl; this.metadataKey = metadataKey + "." + feedUrl; this.feedResource = null; } /** * Creates a FeedEntryMessageSource that will read feeds from the given {@link Resource}. * @param feedResource the {@link Resource} to use. * @param metadataKey the metadata key. * @since 5.0 */ public FeedEntryMessageSource(Resource feedResource, String metadataKey) { Assert.notNull(feedResource, "'feedResource' must not be null"); Assert.notNull(metadataKey, "'metadataKey' must not be null"); this.feedResource = feedResource; this.metadataKey = metadataKey; this.feedUrl = null; } public void setMetadataStore(MetadataStore metadataStore) { Assert.notNull(metadataStore, "'metadataStore' must not be null"); this.metadataStore = metadataStore; } /** * Specify a parser for Feed XML documents. * @param syndFeedInput the {@link SyndFeedInput} to use. * @since 5.0 */ public void setSyndFeedInput(SyndFeedInput syndFeedInput) { Assert.notNull(syndFeedInput, "'syndFeedInput' must not be null"); this.syndFeedInput = syndFeedInput; this.syndFeedInputSet = true; } /** * Specify a flag to indication if {@code WireFeed} should be preserved in the target {@link SyndFeed}. * @param preserveWireFeed the {@code boolean} flag. * @since 5.0 * @see SyndFeedInput#setPreserveWireFeed(boolean) */ public void setPreserveWireFeed(boolean preserveWireFeed) { Assert.isTrue(!this.syndFeedInputSet, "'preserveWireFeed' must be configured on the provided [" + this.syndFeedInput + "]"); this.syndFeedInput.setPreserveWireFeed(preserveWireFeed); } @Override public String getComponentType() { return "feed:inbound-channel-adapter"; } @Override public Message<SyndEntry> receive() { Assert.isTrue(this.initialized, "'FeedEntryReaderMessageSource' must be initialized before it can produce Messages."); SyndEntry entry = doReceive(); if (entry == null) { return null; } return this.getMessageBuilderFactory().withPayload(entry).build(); } @Override protected void onInit() throws Exception { if (this.metadataStore == null) { // first try to look for a 'messageStore' in the context BeanFactory beanFactory = this.getBeanFactory(); if (beanFactory != null) { this.metadataStore = IntegrationContextUtils.getMetadataStore(beanFactory); } // if no 'messageStore' in context, fall back to in-memory Map-based default if (this.metadataStore == null) { this.metadataStore = new SimpleMetadataStore(); } } String lastTimeValue = this.metadataStore.get(this.metadataKey); if (StringUtils.hasText(lastTimeValue)) { this.lastTime = Long.parseLong(lastTimeValue); } this.initialized = true; } private SyndEntry doReceive() { SyndEntry nextEntry = null; synchronized (this.monitor) { nextEntry = getNextEntry(); if (nextEntry == null) { // read feed and try again this.populateEntryList(); nextEntry = getNextEntry(); } } return nextEntry; } private SyndEntry getNextEntry() { SyndEntry next = this.entries.poll(); if (next == null) { return null; } Date lastModifiedDate = FeedEntryMessageSource.getLastModifiedDate(next); if (lastModifiedDate != null) { this.lastTime = lastModifiedDate.getTime(); } else { this.lastTime += 1; //NOSONAR - single poller thread } this.metadataStore.put(this.metadataKey, this.lastTime + ""); return next; } private void populateEntryList() { SyndFeed syndFeed = this.getFeed(); if (syndFeed != null) { List<SyndEntry> retrievedEntries = syndFeed.getEntries(); if (!CollectionUtils.isEmpty(retrievedEntries)) { boolean withinNewEntries = false; Collections.sort(retrievedEntries, this.syndEntryComparator); for (SyndEntry entry : retrievedEntries) { Date entryDate = getLastModifiedDate(entry); if ((entryDate != null && entryDate.getTime() > this.lastTime) || (entryDate == null && withinNewEntries)) { this.entries.add(entry); withinNewEntries = true; } } } } } private SyndFeed getFeed() { try { synchronized (this.feedMonitor) { Reader reader = this.feedUrl != null ? new XmlReader(this.feedUrl) : new XmlReader(this.feedResource.getInputStream()); SyndFeed feed = this.syndFeedInput.build(reader); if (logger.isDebugEnabled()) { logger.debug("Retrieved feed for [" + this + "]"); } if (feed == null) { if (logger.isDebugEnabled()) { logger.debug("No feeds updated for [" + this + "], returning null"); } } return feed; } } catch (Exception e) { throw new MessagingException("Failed to retrieve feed for '" + this + "'", e); } } @Override public String toString() { return "FeedEntryMessageSource{" + "feedUrl=" + this.feedUrl + ", feedResource=" + this.feedResource + ", metadataKey='" + this.metadataKey + '\'' + ", lastTime=" + this.lastTime + '}'; } private static Date getLastModifiedDate(SyndEntry entry) { return (entry.getUpdatedDate() != null) ? entry.getUpdatedDate() : entry.getPublishedDate(); } private static final class SyndEntryPublishedDateComparator implements Comparator<SyndEntry> { SyndEntryPublishedDateComparator() { super(); } @Override public int compare(SyndEntry entry1, SyndEntry entry2) { Date date1 = getLastModifiedDate(entry1); Date date2 = getLastModifiedDate(entry2); if (date1 != null && date2 != null) { return date1.compareTo(date2); } if (date1 == null && date2 == null) { return 0; } return (date2 == null) ? 1 : 0; } } }