/* * 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.twitter.inbound; import java.util.Collections; import java.util.Comparator; import java.util.Date; import java.util.List; import java.util.Queue; import java.util.concurrent.LinkedBlockingQueue; import org.springframework.beans.factory.BeanFactory; 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.jmx.export.annotation.ManagedAttribute; import org.springframework.jmx.export.annotation.ManagedOperation; import org.springframework.messaging.Message; import org.springframework.messaging.MessagingException; import org.springframework.social.twitter.api.DirectMessage; import org.springframework.social.twitter.api.Tweet; import org.springframework.social.twitter.api.Twitter; import org.springframework.social.twitter.api.UserOperations; import org.springframework.util.Assert; import org.springframework.util.CollectionUtils; import org.springframework.util.StringUtils; /** * Abstract class that defines common operations for receiving various types of * messages when using the Twitter API. This class also handles keeping track of * the latest inbound message it has received and avoiding, where possible, * redelivery of duplicate messages. This functionality is enabled using the * {@link org.springframework.integration.metadata.MetadataStore} strategy. * * @author Josh Long * @author Oleg Zhurakousky * @author Mark Fisher * @author Gunnar Hillert * @author Artem Bilan * * @since 2.0 */ @SuppressWarnings("rawtypes") abstract class AbstractTwitterMessageSource<T> extends IntegrationObjectSupport implements MessageSource { private static final int DEFAULT_PAGE_SIZE = 20; private final Twitter twitter; private final TweetComparator tweetComparator = new TweetComparator(); private final Object lastEnqueuedIdMonitor = new Object(); private final String metadataKey; private volatile MetadataStore metadataStore; private final Queue<T> tweets = new LinkedBlockingQueue<T>(); private volatile int prefetchThreshold = 0; private volatile long lastEnqueuedId = -1; private volatile long lastProcessedId = -1; private volatile int pageSize = DEFAULT_PAGE_SIZE; protected AbstractTwitterMessageSource(Twitter twitter, String metadataKey) { Assert.notNull(twitter, "twitter must not be null"); Assert.notNull(metadataKey, "metadataKey must not be null"); this.twitter = twitter; if (this.twitter.isAuthorized()) { UserOperations userOperations = this.twitter.userOperations(); metadataKey += "." + userOperations.getProfileId(); } this.metadataKey = metadataKey; } public void setMetadataStore(MetadataStore metadataStore) { this.metadataStore = metadataStore; } public void setPrefetchThreshold(int prefetchThreshold) { this.prefetchThreshold = prefetchThreshold; } protected Twitter getTwitter() { return this.twitter; } protected int getPageSize() { return this.pageSize; } /** * Set the limit for the number of results returned on each poll; default 20. * @param pageSize The pageSize. */ public void setPageSize(int pageSize) { this.pageSize = pageSize; } @Override protected void onInit() throws Exception { super.onInit(); if (this.metadataStore == null) { // first try to look for a 'metadataStore' in the context BeanFactory beanFactory = this.getBeanFactory(); if (beanFactory != null) { this.metadataStore = IntegrationContextUtils.getMetadataStore(beanFactory); } if (this.metadataStore == null) { this.metadataStore = new SimpleMetadataStore(); } } String lastId = this.metadataStore.get(this.metadataKey); // initialize the last status ID from the metadataStore if (StringUtils.hasText(lastId)) { this.lastProcessedId = Long.parseLong(lastId); this.lastEnqueuedId = this.lastProcessedId; } } @Override public Message<?> receive() { T tweet = this.tweets.poll(); if (tweet == null) { this.refreshTweetQueueIfNecessary(); tweet = this.tweets.poll(); } if (tweet != null) { this.lastProcessedId = this.getIdForTweet(tweet); this.metadataStore.put(this.metadataKey, String.valueOf(this.lastProcessedId)); return this.getMessageBuilderFactory().withPayload(tweet).build(); } return null; } private void enqueueAll(List<T> tweets) { Collections.sort(tweets, this.tweetComparator); for (T tweet : tweets) { enqueue(tweet); } } private void enqueue(T tweet) { synchronized (this.lastEnqueuedIdMonitor) { long id = this.getIdForTweet(tweet); if (id > this.lastEnqueuedId) { this.tweets.add(tweet); this.lastEnqueuedId = id; } } } private void refreshTweetQueueIfNecessary() { try { if (this.tweets.size() <= this.prefetchThreshold) { List<T> tweets = pollForTweets(this.lastEnqueuedId); if (!CollectionUtils.isEmpty(tweets)) { enqueueAll(tweets); } } } catch (RuntimeException e) { throw e; } catch (Exception e) { throw new MessagingException("failed while polling Twitter", e); } } /** * Subclasses must implement this to return tweets. * The 'sinceId' value will be negative if no last id is known. * * @param sinceId The id of the last reported tweet. * @return The list of tweets. */ protected abstract List<T> pollForTweets(long sinceId); private long getIdForTweet(T twitterMessage) { if (twitterMessage instanceof Tweet) { return Long.parseLong(((Tweet) twitterMessage).getId()); } else if (twitterMessage instanceof DirectMessage) { return ((DirectMessage) twitterMessage).getId(); } else { throw new IllegalArgumentException("Unsupported Twitter object: " + twitterMessage); } } /** * Remove the metadata key and the corresponding value from the Metadata Store. */ @ManagedOperation(description = "Remove the metadata key and the corresponding value from the Metadata Store.") void resetMetadataStore() { synchronized (this) { this.metadataStore.remove(this.metadataKey); this.lastProcessedId = -1L; this.lastEnqueuedId = -1L; } } /** * * @return {@code -1} if lastProcessedId is not set, yet. */ @ManagedAttribute public long getLastProcessedId() { return this.lastProcessedId; } private class TweetComparator implements Comparator<T> { TweetComparator() { super(); } @Override public int compare(T tweet1, T tweet2) { // hopefully temporary logic. Will suggest that SpringSocial use a common base class for DM and Tweet if (tweet1 instanceof Tweet && tweet2 instanceof Tweet) { Tweet t1 = (Tweet) tweet1; Tweet t2 = (Tweet) tweet2; Date t1CreatedAt = t1.getCreatedAt(); Date t2CreatedAt = t2.getCreatedAt(); Assert.notNull(t1CreatedAt, "Tweet is missing 'createdAt' date. Cannot compare."); Assert.notNull(t2CreatedAt, "Tweet is missing 'createdAt' date. Cannot compare."); return t1CreatedAt.compareTo(t2CreatedAt); } else if (tweet1 instanceof DirectMessage && tweet2 instanceof DirectMessage) { DirectMessage d1 = (DirectMessage) tweet1; DirectMessage d2 = (DirectMessage) tweet2; Date d1CreatedAt = d1.getCreatedAt(); Date d2CreatedAt = d2.getCreatedAt(); Assert.notNull(d1CreatedAt, "DirectMessage is missing 'createdAt' date. Cannot compare."); Assert.notNull(d2CreatedAt, "DirectMessage is missing 'createdAt' date. Cannot compare."); return d1CreatedAt.compareTo(d2CreatedAt); } else { throw new IllegalArgumentException("Uncomparable Twitter objects: " + tweet1 + " and " + tweet2); } } } }