Java tutorial
/** * Timeline * Copyright 22.02.2015 by Michael Peter Christen, @0rb1t3r * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program in the file lgpl21.txt * If not, see <http://www.gnu.org/licenses/>. */ package org.loklak.objects; import java.util.ArrayList; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.NavigableMap; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentSkipListMap; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; import org.loklak.QueuedIndexing; import org.loklak.data.DAO; import org.loklak.data.DAO.IndexName; import org.loklak.susi.SusiThought; /** * A timeline is a structure which holds tweet for the purpose of presentation * There is no tweet retrieval method here, just an iterator which returns the tweets in reverse appearing order */ public class Timeline implements Iterable<MessageEntry> { public static enum Order { CREATED_AT("date"), RETWEET_COUNT("long"), FAVOURITES_COUNT("long"); String field_type; Order(String field_type) { this.field_type = field_type; } public String getMessageFieldName() { return this.name().toLowerCase(); } public String getMessageFieldType() { return this.field_type; } } private NavigableMap<String, MessageEntry> tweets; // the key is the date plus id of the tweet private Map<String, UserEntry> users; private int hits = -1; private String scraperInfo = ""; final private Order order; private String query; private IndexName indexName; public Timeline(Order order) { this.tweets = new ConcurrentSkipListMap<String, MessageEntry>(); this.users = new ConcurrentHashMap<String, UserEntry>(); this.order = order; this.query = null; this.indexName = null; } public Timeline(Order order, String scraperInfo) { this(order); this.scraperInfo = scraperInfo; } public static Order parseOrder(String order) { try { return Order.valueOf(order.toUpperCase()); } catch (Throwable e) { return Order.CREATED_AT; } } public void clear() { this.tweets.clear(); this.users.clear(); // we keep the other details (like order, scraperInfo and query) to be able to test with zero-size pushes } public void setResultIndex(IndexName index) { this.indexName = index; } public IndexName getResultIndex() { return this.indexName; } public void setScraperInfo(String info) { this.scraperInfo = info; } public String getScraperInfo() { return this.scraperInfo; } public Order getOrder() { return this.order; } public String getQuery() { return this.query; } public void setQuery(String query) { this.query = query; } public int size() { return this.tweets.size(); } public Timeline reduceToMaxsize(final int maxsize) { List<MessageEntry> m = new ArrayList<>(); Timeline t = new Timeline(this.order); if (maxsize < 0) return t; // remove tweets from this timeline synchronized (tweets) { while (this.tweets.size() > maxsize) m.add(this.tweets.remove(this.tweets.firstEntry().getKey())); } // create new timeline for (MessageEntry me : m) { t.addUser(this.users.get(me.getScreenName())); t.addTweet(me); } // prune away users not needed any more in this structure Set<String> screen_names = new HashSet<>(); for (MessageEntry me : this.tweets.values()) screen_names.add(me.getScreenName()); synchronized (this.users) { Iterator<Map.Entry<String, UserEntry>> i = this.users.entrySet().iterator(); while (i.hasNext()) { Map.Entry<String, UserEntry> e = i.next(); if (!screen_names.contains(e.getValue().getScreenName())) i.remove(); } } return t; } public void add(MessageEntry tweet, UserEntry user) { this.addUser(user); this.addTweet(tweet); } private void addUser(UserEntry user) { assert user != null; if (user != null) this.users.put(user.getScreenName(), user); } private void addTweet(MessageEntry tweet) { String key = ""; if (this.order == Order.RETWEET_COUNT) { key = Long.toHexString(tweet.getRetweetCount()); while (key.length() < 16) key = "0" + key; key = key + "_" + tweet.getIdStr(); } else if (this.order == Order.FAVOURITES_COUNT) { key = Long.toHexString(tweet.getFavouritesCount()); while (key.length() < 16) key = "0" + key; key = key + "_" + tweet.getIdStr(); } else { key = Long.toHexString(tweet.getCreatedAt().getTime()) + "_" + tweet.getIdStr(); } synchronized (tweets) { this.tweets.put(key, tweet); } } protected UserEntry getUser(String user_screen_name) { return this.users.get(user_screen_name); } public UserEntry getUser(MessageEntry fromTweet) { return this.users.get(fromTweet.getScreenName()); } public void putAll(Timeline other) { if (other == null) return; assert this.order.equals(other.order); for (Map.Entry<String, UserEntry> u : other.users.entrySet()) { UserEntry t = this.users.get(u.getKey()); if (t == null || !t.containsProfileImage()) { this.users.put(u.getKey(), u.getValue()); } } for (MessageEntry t : other) this.addTweet(t); } public MessageEntry getBottomTweet() { synchronized (tweets) { return this.tweets.firstEntry().getValue(); } } public MessageEntry getTopTweet() { synchronized (tweets) { return this.tweets.lastEntry().getValue(); } } public String toString() { return toJSON(true, "search_metadata", "statuses").toString(); //return new ObjectMapper().writer().writeValueAsString(toMap(true)); } public JSONObject toJSON(boolean withEnrichedData, String metadata_field_name, String statuses_field_name) throws JSONException { JSONObject json = toSusi(withEnrichedData, metadata_field_name, statuses_field_name); json.getJSONObject(metadata_field_name).put("count", Integer.toString(this.tweets.size())); json.put("peer_hash", DAO.public_settings.getPeerHash()); json.put("peer_hash_algorithm", DAO.public_settings.getPeerHashAlgorithm()); return json; } public SusiThought toSusi(boolean withEnrichedData, String metadata_field_name, String statuses_field_name) throws JSONException { return toSusi(withEnrichedData, new SusiThought(metadata_field_name, statuses_field_name)); } public SusiThought toSusi(boolean withEnrichedData) throws JSONException { return toSusi(withEnrichedData, new SusiThought()); } private SusiThought toSusi(boolean withEnrichedData, SusiThought json) throws JSONException { json.setQuery(this.query).setHits(Math.max(this.hits, this.size())); if (this.scraperInfo.length() > 0) json.setScraperInfo(this.scraperInfo); JSONArray statuses = new JSONArray(); for (MessageEntry t : this) { UserEntry u = this.users.get(t.getScreenName()); statuses.put(t.toJSON(u, withEnrichedData, Integer.MAX_VALUE, "")); } json.setData(statuses); return json; } /** * the tweet iterator returns tweets in descending appearance order (top first) */ @Override public Iterator<MessageEntry> iterator() { return this.tweets.descendingMap().values().iterator(); } /** * compute the average time between any two consecutive tweets * @return time in milliseconds */ public long period() { if (this.size() < 1) return Long.MAX_VALUE; // calculate the time based on the latest 20 tweets (or less) long latest = 0; long earliest = 0; int count = 0; for (MessageEntry messageEntry : this) { if (latest == 0) { latest = messageEntry.created_at.getTime(); continue; } earliest = messageEntry.created_at.getTime(); count++; if (count >= 19) break; } if (count == 0) return Long.MAX_VALUE; long timeInterval = latest - earliest; long p = 1 + timeInterval / count; return p < 4000 ? p / 4 + 3000 : p; } public void writeToIndex() { QueuedIndexing.addScheduler(this, true); } public void setHits(int hits) { this.hits = hits; } public int getHits() { return this.hits == -1 ? this.size() : this.hits; } }