/*
* 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.converter.util;
import org.apache.streams.exceptions.ActivityConversionException;
import org.apache.streams.jackson.StreamsJacksonMapper;
import org.apache.streams.pojo.extensions.ExtensionUtil;
import org.apache.streams.pojo.json.Activity;
import org.apache.streams.pojo.json.ActivityObject;
import org.apache.streams.pojo.json.Image;
import org.apache.streams.pojo.json.Provider;
import org.apache.streams.twitter.Url;
import org.apache.streams.twitter.pojo.Delete;
import org.apache.streams.twitter.pojo.Entities;
import org.apache.streams.twitter.pojo.Hashtag;
import org.apache.streams.twitter.pojo.Place;
import org.apache.streams.twitter.pojo.Retweet;
import org.apache.streams.twitter.pojo.TargetObject;
import org.apache.streams.twitter.pojo.Tweet;
import org.apache.streams.twitter.pojo.User;
import org.apache.streams.twitter.pojo.UserMentions;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import static com.google.common.math.DoubleMath.mean;
/**
* Provides utilities for working with Activity objects within the context of Twitter.
*/
public class TwitterActivityUtil {
private static final Logger LOGGER = LoggerFactory.getLogger(TwitterActivityUtil.class);
private static final ObjectMapper mapper = StreamsJacksonMapper.getInstance();
/**
* Updates the given Activity object with the values from the Tweet.
* @param tweet the object to use as the source
* @param activity the target of the updates. Will receive all values from the tweet.
* @throws ActivityConversionException ActivityConversionException
*/
public static void updateActivity(Tweet tweet, Activity activity) throws ActivityConversionException {
activity.setActor(buildActor(tweet));
activity.setId(formatId(activity.getVerb(),
Optional.ofNullable(Optional.ofNullable(tweet.getIdStr())
.orElseGet(Optional.of(tweet.getId().toString())::get)).orElse(null)));
if (tweet instanceof Retweet) {
updateActivityContent(activity, (tweet).getRetweetedStatus(), "share");
} else {
updateActivityContent(activity, tweet, "post");
}
if (StringUtils.isBlank(activity.getId())) {
throw new ActivityConversionException("Unable to determine activity id");
}
try {
activity.setPublished(tweet.getCreatedAt());
} catch ( Exception ex ) {
throw new ActivityConversionException("Unable to determine publishedDate", ex);
}
activity.setTarget(buildTarget(tweet));
activity.setProvider(getProvider());
activity.setUrl(String.format("http://twitter.com/%s/%s/%s", tweet.getUser().getScreenName(),"/status/",tweet.getIdStr()));
addTwitterExtension(activity, mapper.convertValue(tweet, ObjectNode.class));
}
/**
* Updates the given Activity object with the values from the User
* @param user the object to use as the source
* @param activity the target of the updates. Will receive all values from the tweet.
*/
public static void updateActivity(User user, Activity activity) {
activity.setActor(buildActor(user));
activity.setId(null);
activity.setVerb(null);
}
/**
* Updates the activity for a delete event.
* @param delete the delete event
* @param activity the Activity object to update
* @throws ActivityConversionException ActivityConversionException
*/
public static void updateActivity(Delete delete, Activity activity) throws ActivityConversionException {
activity.setActor(buildActor(delete));
activity.setVerb("delete");
activity.setObject(buildActivityObject(delete));
activity.setId(formatId(activity.getVerb(), delete.getDelete().getStatus().getIdStr()));
if (StringUtils.isBlank(activity.getId())) {
throw new ActivityConversionException("Unable to determine activity id");
}
activity.setProvider(getProvider());
addTwitterExtension(activity, StreamsJacksonMapper.getInstance().convertValue(delete, ObjectNode.class));
}
/**
* Builds the activity {@link ActivityObject} actor from the tweet.
* @param tweet the object to use as the source
* @return a valid Actor populated from the Tweet
*/
public static ActivityObject buildActor(Tweet tweet) {
ActivityObject actor = new ActivityObject();
User user = tweet.getUser();
return buildActor(user);
}
/**
* Builds the activity {@link ActivityObject} actor from the User.
* @param user the object to use as the source
* @return a valid Actor populated from the Tweet
*/
public static ActivityObject buildActor(User user) {
ActivityObject actor = new ActivityObject();
actor.setId(formatId(
Optional.ofNullable(Optional.ofNullable(user.getIdStr())
.orElseGet(Optional.of(user.getId().toString())::get)).orElse(null)
));
actor.setObjectType("page");
actor.setDisplayName(user.getName());
actor.setAdditionalProperty("handle", user.getScreenName());
actor.setSummary(user.getDescription());
if (user.getUrl() != null) {
actor.setUrl(user.getUrl());
}
Map<String, Object> extensions = new HashMap<>();
extensions.put("location", user.getLocation());
extensions.put("posts", user.getStatusesCount());
extensions.put("favorites", user.getFavouritesCount());
extensions.put("followers", user.getFollowersCount());
Image profileImage = new Image();
profileImage.setUrl(user.getProfileImageUrlHttps());
actor.setImage(profileImage);
extensions.put("screenName", user.getScreenName());
actor.setAdditionalProperty("extensions", extensions);
return actor;
}
/**
* Builds the actor for a delete event.
* @param delete the delete event
* @return a valid Actor
*/
public static ActivityObject buildActor(Delete delete) {
ActivityObject actor = new ActivityObject();
actor.setId(formatId(delete.getDelete().getStatus().getUserIdStr()));
actor.setObjectType("page");
return actor;
}
/**
* Creates an {@link ActivityObject} for the tweet.
* @param tweet the object to use as the source
* @return a valid ActivityObject
*/
public static ActivityObject buildActivityObject(Tweet tweet) {
ActivityObject actObj = new ActivityObject();
String id = Optional.ofNullable(Optional.ofNullable(tweet.getIdStr())
.orElseGet(Optional.of(tweet.getId().toString())::get)).orElse(null);
if ( id != null ) {
actObj.setId(id);
}
actObj.setObjectType("post");
actObj.setContent(tweet.getText());
return actObj;
}
/**
* Builds the ActivityObject for the delete event.
* @param delete the delete event
* @return a valid Activity Object
*/
public static ActivityObject buildActivityObject(Delete delete) {
ActivityObject actObj = new ActivityObject();
actObj.setId(formatId(delete.getDelete().getStatus().getIdStr()));
actObj.setObjectType("tweet");
return actObj;
}
/**
* Updates the content, and associated fields, with those from the given tweet
* @param activity the target of the updates. Will receive all values from the tweet.
* @param tweet the object to use as the source
* @param verb the verb for the given activity's type
*/
public static void updateActivityContent(Activity activity, Tweet tweet, String verb) {
activity.setVerb(verb);
activity.setTitle("");
if ( tweet != null ) {
activity.setObject(buildActivityObject(tweet));
activity.setLinks(getLinks(tweet));
activity.setContent(tweet.getText());
addLocationExtension(activity, tweet);
addTwitterExtensions(activity, tweet);
}
}
/**
* Gets the links from the Twitter event
* @param tweet the object to use as the source
* @return a list of links corresponding to the expanded URL (no t.co)
*/
public static List<String> getLinks(Tweet tweet) {
List<String> links = new ArrayList<>();
if ( tweet.getEntities().getUrls() != null ) {
for (Url url : tweet.getEntities().getUrls()) {
links.add(url.getExpandedUrl());
}
} else {
LOGGER.debug(" 0 links");
}
return links;
}
/**
* Builds the {@link TargetObject} from the tweet.
* @param tweet the object to use as the source
* @return currently returns null for all activities
*/
public static ActivityObject buildTarget(Tweet tweet) {
return null;
}
/**
* Adds the location extension and populates with teh twitter data.
* @param activity the Activity object to update
* @param tweet the object to use as the source
*/
public static void addLocationExtension(Activity activity, Tweet tweet) {
Map<String, Object> extensions = ExtensionUtil.getInstance().ensureExtensions(activity);
Map<String, Object> location = new HashMap<>();
location.put("id", formatId(
Optional.ofNullable(Optional.ofNullable(tweet.getIdStr())
.orElseGet(Optional.of(tweet.getId().toString())::get)).orElse(null)
));
location.put("coordinates", boundingBoxCenter(tweet.getPlace()));
extensions.put("location", location);
}
/**
* Gets the common twitter {@link Provider} object
* @return a provider object representing Twitter
*/
public static Provider getProvider() {
Provider provider = new Provider();
provider.setId("id:providers:twitter");
provider.setObjectType("application");
provider.setDisplayName("Twitter");
return provider;
}
/**
* Adds the given Twitter event to the activity as an extension.
* @param activity the Activity object to update
* @param event the Twitter event to add as the extension
*/
public static void addTwitterExtension(Activity activity, ObjectNode event) {
Map<String, Object> extensions = ExtensionUtil.getInstance().ensureExtensions(activity);
extensions.put("twitter", event);
}
/**
* Formats the ID to conform with the Apache Streams activity ID convention.
* @param idparts the parts of the ID to join
* @return a valid Activity ID in format "id:twitter:part1:part2:...partN"
*/
public static String formatId(String... idparts) {
return String.join(":", Stream.concat(Arrays.stream(new String[]{"id:twitter"}), Arrays.stream(idparts)).collect(Collectors.toList()));
}
/**
* Takes various parameters from the twitter object that are currently not part of the
* activity schema and stores them in a generic extensions attribute.
* @param activity Activity
* @param tweet Tweet
*/
public static void addTwitterExtensions(Activity activity, Tweet tweet) {
Map<String, Object> extensions = ExtensionUtil.getInstance().ensureExtensions(activity);
List<String> hashtags = new ArrayList<>();
for (Hashtag hashtag : tweet.getEntities().getHashtags()) {
hashtags.add(hashtag.getText());
}
extensions.put("hashtags", hashtags);
Map<String, Object> likes = new HashMap<>();
likes.put("perspectival", tweet.getFavorited());
likes.put("count", tweet.getAdditionalProperties().get("favorite_count"));
extensions.put("likes", likes);
Map<String, Object> rebroadcasts = new HashMap<>();
rebroadcasts.put("perspectival", tweet.getRetweeted());
rebroadcasts.put("count", tweet.getRetweetCount());
extensions.put("rebroadcasts", rebroadcasts);
List<Map<String, Object>> userMentions = new ArrayList<>();
Entities entities = tweet.getEntities();
for (UserMentions user : entities.getUserMentions()) {
//Map the twitter user object into an actor
Map<String, Object> actor = new HashMap<>();
actor.put("id", "id:twitter:" + user.getIdStr());
actor.put("displayName", user.getName());
actor.put("handle", user.getScreenName());
userMentions.add(actor);
}
extensions.put("user_mentions", userMentions);
extensions.put("keywords", tweet.getText());
}
/**
* Compute central coordinates from bounding box.
* @param place the bounding box to use as the source
*/
public static List<Double> boundingBoxCenter(Place place) {
if ( place == null ) {
return new ArrayList<>();
}
if ( place.getBoundingBox() == null ) {
return new ArrayList<>();
}
if ( place.getBoundingBox().getCoordinates().size() != 1 ) {
return new ArrayList<>();
}
if ( place.getBoundingBox().getCoordinates().get(0).size() != 4 ) {
return new ArrayList<>();
}
List<Double> lats = new ArrayList<>();
List<Double> lons = new ArrayList<>();
for ( List<Double> point : place.getBoundingBox().getCoordinates().get(0)) {
lats.add(point.get(0));
lons.add(point.get(1));
}
List<Double> result = new ArrayList<>();
result.add(mean(lats));
result.add(mean(lons));
return result;
}
}