/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* 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.apache.streams.twitter.processor;
import org.apache.streams.config.ComponentConfigurator;
import org.apache.streams.config.StreamsConfigurator;
import org.apache.streams.core.StreamsDatum;
import org.apache.streams.core.StreamsProcessor;
import org.apache.streams.exceptions.ActivityConversionException;
import org.apache.streams.jackson.StreamsJacksonMapper;
import org.apache.streams.pojo.json.Activity;
import org.apache.streams.twitter.TwitterConfiguration;
import org.apache.streams.twitter.TwitterStreamConfiguration;
import org.apache.streams.twitter.api.StatusesShowRequest;
import org.apache.streams.twitter.api.Twitter;
import org.apache.streams.twitter.converter.TwitterDocumentClassifier;
import org.apache.streams.twitter.pojo.Delete;
import org.apache.streams.twitter.pojo.Retweet;
import org.apache.streams.twitter.pojo.Tweet;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import static org.apache.streams.twitter.converter.util.TwitterActivityUtil.getProvider;
import static org.apache.streams.twitter.converter.util.TwitterActivityUtil.updateActivity;
/**
* Given an Activity, fetches the tweet by the activity object id and replaces the existing activity with the converted activity
* from what is returned by the twitter API.
*/
public class FetchAndReplaceTwitterProcessor implements StreamsProcessor {
private static final String PROVIDER_ID = getProvider().getId();
private static final Logger LOGGER = LoggerFactory.getLogger(FetchAndReplaceTwitterProcessor.class);
//Default number of attempts before allowing the document through
private static final int MAX_ATTEMPTS = 5;
//Start the backoff at 4 minutes. This results in a wait period of 4, 8, 12, 16 & 20 min with an attempt of 5
public static final int BACKOFF = 1000 * 60 * 4;
private final TwitterConfiguration config;
private Twitter client;
private ObjectMapper mapper;
private int retryCount;
public FetchAndReplaceTwitterProcessor() {
this(new ComponentConfigurator<>(TwitterStreamConfiguration.class).detectConfiguration(StreamsConfigurator.config, "twitter"));
}
public FetchAndReplaceTwitterProcessor(TwitterStreamConfiguration config) {
this.config = config;
}
@Override
public String getId() {
return getProvider().getId();
}
@Override
public List<StreamsDatum> process(StreamsDatum entry) {
if (entry.getDocument() instanceof Activity) {
Activity doc = (Activity)entry.getDocument();
String originalId = doc.getId();
if (PROVIDER_ID.equals(doc.getProvider().getId())) {
try {
fetchAndReplace(doc, originalId);
} catch (ActivityConversionException ex) {
LOGGER.warn("ActivityConversionException", ex);
} catch (IOException ex) {
LOGGER.warn("IOException", ex);
}
}
} else {
throw new IllegalStateException("Requires an activity document");
}
return Stream.of(entry).collect(Collectors.toList());
}
@Override
public void prepare(Object configurationObject) {
try {
client = getTwitterClient();
} catch (InstantiationException e) {
LOGGER.error("InstantiationException", e);
}
Objects.requireNonNull(client);
this.mapper = StreamsJacksonMapper.getInstance();
Objects.requireNonNull(mapper);
}
@Override
public void cleanUp() {
}
protected void fetchAndReplace(Activity doc, String originalId) throws java.io.IOException, ActivityConversionException {
Tweet tweet = fetch(doc);
replace(doc, tweet);
doc.setId(originalId);
}
protected void replace(Activity doc, Tweet tweet) throws java.io.IOException, ActivityConversionException {
String json = mapper.writeValueAsString(tweet);
Class documentSubType = new TwitterDocumentClassifier().detectClasses(json).get(0);
Object object = mapper.readValue(json, documentSubType);
if (documentSubType.equals(Retweet.class) || documentSubType.equals(Tweet.class)) {
updateActivity((Tweet)object, doc);
} else if (documentSubType.equals(Delete.class)) {
updateActivity((Delete)object, doc);
} else {
LOGGER.info("Could not determine the correct update method for {}", documentSubType);
}
}
protected Tweet fetch(Activity doc) {
String id = doc.getObject().getId();
LOGGER.debug("Fetching status from Twitter for {}", id);
Long tweetId = Long.valueOf(id.replace("id:twitter:tweets:", ""));
Tweet tweet = client.show(
new StatusesShowRequest()
.withId(tweetId)
);
return tweet;
}
protected Twitter getTwitterClient() throws InstantiationException {
return Twitter.getInstance(config);
}
//Hardcore sleep to allow for catch up
// protected void sleepAndTryAgain(Activity doc, String originalId) {
// try {
// //Attempt to fetchAndReplace with a backoff up to the limit then just reset the count and let the process continue
// if (retryCount < MAX_ATTEMPTS) {
// retryCount++;
// LOGGER.debug("Sleeping for {} min due to excessive calls to Twitter API", (retryCount * 4));
// Thread.sleep(BACKOFF * retryCount);
// fetchAndReplace(doc, originalId);
// } else {
// retryCount = 0;
// }
// } catch (InterruptedException ex) {
// LOGGER.warn("Thread sleep interrupted while waiting for twitter backoff");
// }
// }
}