diff --git a/pom.xml b/pom.xml index d151190..4a7f74f 100644 --- a/pom.xml +++ b/pom.xml @@ -25,7 +25,7 @@ - 2.5.1 + 2.7.0 distro 6.2.5 @@ -52,10 +52,18 @@ com.google.gwt gwt-user + ${gwtVersion} com.google.gwt gwt-servlet + ${gwtVersion} + provided + + + com.google.gwt + gwt-dev + ${gwtVersion} provided @@ -106,6 +114,10 @@ portlet-api provided + + com.github.gwtbootstrap + gwt-bootstrap + diff --git a/src/main/java/org/gcube/portlets/user/topics/client/panel/TopicsPanel.java b/src/main/java/org/gcube/portlets/user/topics/client/panel/TopicsPanel.java index 78687ec..b86d880 100644 --- a/src/main/java/org/gcube/portlets/user/topics/client/panel/TopicsPanel.java +++ b/src/main/java/org/gcube/portlets/user/topics/client/panel/TopicsPanel.java @@ -6,7 +6,10 @@ import org.gcube.portlets.user.topics.client.TopicService; import org.gcube.portlets.user.topics.client.TopicServiceAsync; import org.gcube.portlets.user.topics.shared.HashtagsWrapper; +import com.github.gwtbootstrap.client.ui.Button; import com.google.gwt.core.client.GWT; +import com.google.gwt.event.dom.client.ClickEvent; +import com.google.gwt.event.dom.client.ClickHandler; import com.google.gwt.user.client.rpc.AsyncCallback; import com.google.gwt.user.client.ui.Composite; import com.google.gwt.user.client.ui.HTML; @@ -27,6 +30,7 @@ public class TopicsPanel extends Composite { public static final String loading = GWT.getModuleBaseURL() + "../images/topics-loader.gif"; public static final String DISPLAY_NAME = "Top Topics"; + public static final int THRESHOLD_SHOW_HASHTAGS = 10; // show the first X ones private Image loadingImage; @@ -50,13 +54,42 @@ public class TopicsPanel extends Composite { showServError(); } else { + int counter = 0; if (hashtags != null) { for (String hashtag : hashtags) { + counter ++; HTML toAdd = new HTML(hashtag); toAdd.addStyleName("hashtag-label"); mainPanel.add(toAdd); + + if(counter > THRESHOLD_SHOW_HASHTAGS) // 11, 12... + toAdd.setVisible(false); } } + + // add a show all button if needed + if(counter > THRESHOLD_SHOW_HASHTAGS){ + + final Button showAllHashtags = new Button("Show All"); + + showAllHashtags.addClickHandler(new ClickHandler() { + + @Override + public void onClick(ClickEvent event) { + + int numberChildren = mainPanel.getWidgetCount(); + for (int i = THRESHOLD_SHOW_HASHTAGS; i < numberChildren; i++) { + + mainPanel.getWidget(i).setVisible(true); + + } + + // hide the button + showAllHashtags.setVisible(false); + } + }); + mainPanel.add(showAllHashtags); + } } } diff --git a/src/main/java/org/gcube/portlets/user/topics/server/TopicServiceImpl.java b/src/main/java/org/gcube/portlets/user/topics/server/TopicServiceImpl.java index 7498f54..477c9b1 100644 --- a/src/main/java/org/gcube/portlets/user/topics/server/TopicServiceImpl.java +++ b/src/main/java/org/gcube/portlets/user/topics/server/TopicServiceImpl.java @@ -1,9 +1,16 @@ package org.gcube.portlets.user.topics.server; +import java.text.SimpleDateFormat; import java.util.ArrayList; +import java.util.Calendar; import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.LinkedList; import java.util.List; import java.util.Map; +import java.util.Map.Entry; import org.apache.commons.codec.binary.Base64; import org.gcube.application.framework.core.session.ASLSession; @@ -12,6 +19,7 @@ import org.gcube.portal.custom.scopemanager.scopehelper.ScopeHelper; import org.gcube.portal.databook.client.GCubeSocialNetworking; import org.gcube.portal.databook.server.DBCassandraAstyanaxImpl; import org.gcube.portal.databook.server.DatabookStore; +import org.gcube.portal.databook.shared.Feed; import org.gcube.portlets.user.topics.client.TopicService; import org.gcube.portlets.user.topics.shared.HashTagAndOccurrence; import org.gcube.portlets.user.topics.shared.HashtagsWrapper; @@ -34,6 +42,8 @@ public class TopicServiceImpl extends RemoteServiceServlet implements TopicServi private static final Logger _log = LoggerFactory.getLogger(TopicServiceImpl.class); public static final String TEST_USER = "test.user"; + private static final int WINDOW_SIZE_IN_MONTHS = 6; // it must not exceed 12 + /** * The Cassandra store interface */ @@ -68,20 +78,32 @@ public class TopicServiceImpl extends RemoteServiceServlet implements TopicServi */ public String getDevelopmentUser() { String user = TEST_USER; - //user = "massimiliano.assante"; + // user = "massimiliano.assante"; return user; } /** - * return the top 10 hashtag with max occurrence + * return trending hashtags */ @Override public HashtagsWrapper getHashtags() { ArrayList hashtagsChart = new ArrayList<>(); ASLSession session = getASLSession(); + + long timestampStart = System.currentTimeMillis(); + + // get the reference time + Calendar referenceTime = Calendar.getInstance(); + int currentMonth = referenceTime.get(Calendar.MONTH); // jan = 0, ..... dec = 11 + referenceTime.set(Calendar.MONTH, currentMonth - WINDOW_SIZE_IN_MONTHS); // the year is automatically decreased if needed + + // print it + SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd"); + _log.debug("Reference time for trending topics is " + format.format(referenceTime.getTime())); String userName = session.getUsername(); boolean isInfrastructure = isInfrastructureScope(); + try { //in case the portal is restarted and you have the social home open it will get test.user (no callback to set session info) //this check just return nothing if that happens @@ -90,41 +112,87 @@ public class TopicServiceImpl extends RemoteServiceServlet implements TopicServi return null; } ArrayList toSort = new ArrayList(); - /** - * this handles the case where the portlet is deployed outside of VREs (regular) - */ - if (isInfrastructure) { _log.debug("****** retrieving hashtags for user VREs"); + + // different vres could have a same hashtag, we need to merge them + Map hashtags = new HashMap(); + + // we need a map for the couple + // it is needed because later we need to retrieve the most recent feed among the ones + // containing the hashtag itself + Map> hashtagsInVres = new HashMap>(); + GroupManager gm = new LiferayGroupManager(); UserManager um = new LiferayUserManager(); GCubeUser user = um.getUserByUsername(userName); + List groups = gm.listGroupsByUser(user.getUserId()); for (GCubeGroup group : groups) { if (gm.isVRE(group.getGroupId())) { - _log.debug("Retrieving hashtags from VRE " + group.getGroupName()); String vreid = gm.getInfrastructureScope(group.getGroupId()); //get the scope - Map map = store.getVREHashtagsWithOccurrence(vreid); + Map map = store.getVREHashtagsWithOccurrenceFilteredByTime(vreid, referenceTime.getTimeInMillis()); + + // merge the values if needed for (String hashtag : map.keySet()) { - toSort.add(new HashTagAndOccurrence(hashtag, map.get(hashtag))); + + if(hashtags.containsKey(hashtag)){ + + int currentValue = hashtags.get(hashtag); + int newValue = currentValue + map.get(hashtag); + + // remove and re-add + hashtags.remove(hashtag); + hashtags.put(hashtag, newValue); + + // get the current list of vres in which the hashtag is present and add this new one + List vres = hashtagsInVres.get(hashtag); + vres.add(vreid); + hashtagsInVres.remove(hashtag); + hashtagsInVres.put(hashtag, vres); + + }else{ + + hashtags.put(hashtag, map.get(hashtag)); + + // put in the hashmap hashtagsInVres too + List vres = new ArrayList(); + vres.add(vreid); + hashtagsInVres.put(hashtag, vres); + } } } - } + } + + // now we need to evaluate score for each element + Map weights = evaluateWeight(hashtags, WINDOW_SIZE_IN_MONTHS, currentMonth, referenceTime, null, hashtagsInVres); + + // at the end build the list + for (String hashtag : hashtags.keySet()) { + toSort.add(new HashTagAndOccurrence(hashtag, hashtags.get(hashtag), weights.get(hashtag))); + } + } //else must be in a VRE scope else { String scope = session.getScope(); _log.debug("****** retrieving hashtags for scope " + scope); - Map map = store.getVREHashtagsWithOccurrence(scope); - for (String hashtag : map.keySet()) { - toSort.add(new HashTagAndOccurrence(hashtag, map.get(hashtag))); + Map hashtags = store.getVREHashtagsWithOccurrenceFilteredByTime(scope, referenceTime.getTimeInMillis()); + // now we need to evaluate the wiehgt for each element + Map weights = evaluateWeight(hashtags, WINDOW_SIZE_IN_MONTHS, currentMonth, referenceTime, scope, null); + for (String hashtag : hashtags.keySet()) { + toSort.add(new HashTagAndOccurrence(hashtag, hashtags.get(hashtag), weights.get(hashtag))); } } _log.debug("Number of topics retrieved is " + toSort.size()); - Collections.sort(toSort, Collections.reverseOrder()); - int i = 0; + + Collections.sort(toSort); // sort for weight + for (HashTagAndOccurrence wrapper : toSort) { + + _log.debug("Entry is " + wrapper.toString() + " with weight " + wrapper.getWeight()); + String hashtag = wrapper.getHashtag(); String href="\"?"+ @@ -132,19 +200,165 @@ public class TopicServiceImpl extends RemoteServiceServlet implements TopicServi new String(Base64.encodeBase64(hashtag.getBytes()))+"\""; String hashtagLink = ""+hashtag+""; hashtagsChart.add(hashtagLink); - i++; - if (i >= 10) - break; } } catch (Exception e) { e.printStackTrace(); return null; } + + + long timestampEnd = System.currentTimeMillis() - timestampStart; + _log.debug("Overall time to retrieve hastags is " + timestampEnd); + return new HashtagsWrapper(isInfrastructure, hashtagsChart); } + /** + * Evaluate the weight for each element as w = 0.6 * s + 0.4 * f + * where s is the score: a normalized value given by counter_i / counter_max + * f is the freshness: evaluated taking into account the most recent feed containing that hashtag into the window w (that is, + * the period taken into account) + * @param hashtags + * @param hashtagsInVres (present if vreid is null) + * @param window size + * @param current month + * @param referenceTime + * @param vreid (present if hashtagsInVres is null) + * @return a Map of weight for each hashtag + */ + private Map evaluateWeight(Map hashtags, int windowSize, int currentMonth, Calendar referenceTime, String vreId, Map> hashtagsInVres) { + + Map weights = new HashMap(); + + // find max score inside the list (counter) + int max = 0; + for(Entry entry : hashtags.entrySet()){ + + max = max < entry.getValue() ? entry.getValue() : max; + + } + + // normalize + Map normalized = new HashMap(); + for(Entry entry : hashtags.entrySet()){ + + normalized.put(entry.getKey(), (double)entry.getValue() / (double)max); + + } + + // create the weight for each entry as: + // w = 0.6 * normalized_score + 0.4 * freshness + // freshness is evaluated as (window_size - latest_feed_for_hashtag_in_window_month)/window_size + for(Entry entry : hashtags.entrySet()){ + + // first part of the weight + double weight = 0.6 * normalized.get(entry.getKey()); + + List mostRecentFeedForHashtag = null; + + // we are in the simplest case.. the hashtag belongs (or the request comes) from a single vre + if(vreId != null){ + + try{ + + mostRecentFeedForHashtag = store.getVREFeedsByHashtag(vreId, entry.getKey()); + + }catch(Exception e){ + + _log.error("Unable to retrieve the most recent feeds for hashtag " + entry.getKey() + " in " + vreId); + + // put a weight of zero for this hashtag + weights.put(entry.getKey(), 0.0); + continue; + } + + }else{ // we are not so lucky + + // get the list of vres for this hashtag + List vres = hashtagsInVres.get(entry.getKey()); + + // init list + mostRecentFeedForHashtag = new ArrayList(); + + List feedsForVre; + for (String vre : vres) { + try{ + feedsForVre = store.getVREFeedsByHashtag(vre, entry.getKey()); + }catch(Exception e){ + _log.error("Unable to retrieve the most recent feeds for hashtag " + entry.getKey() + " in " + vreId); + continue; + } + + // add to the list + mostRecentFeedForHashtag.addAll(feedsForVre); + } + + // check if there is at least a feed or it is empty + if(mostRecentFeedForHashtag.isEmpty()){ + // put a weight of zero for this hashtag + weights.put(entry.getKey(), 0.0); + continue; + } + } + + // retrieve the most recent one among these feeds + Collections.sort(mostRecentFeedForHashtag, Collections.reverseOrder()); + + // get month of the last recent feed for this hashtag + Calendar monstRecentFeedForHashTagTime = Calendar.getInstance(); + monstRecentFeedForHashTagTime.setTimeInMillis(mostRecentFeedForHashtag.get(0).getTime().getTime()); + + int sub = currentMonth - monstRecentFeedForHashTagTime.get(Calendar.MONTH); + int value = sub >= 0? sub : 12 - Math.abs(sub); + double freshness = 1.0 - (double)(value) / (double)(windowSize); + _log.debug("freshness is " + freshness + " for hashtag " + entry.getKey() + + " because the last feed has month " + monstRecentFeedForHashTagTime.get(Calendar.MONTH)); + + // update the weight + weight += 0.4 * freshness; + + // put it into the hashmap + weights.put(entry.getKey(), weight); + } + + // print sorted + Map scoredListSorted = sortByWeight(weights); + for(Entry entry : scoredListSorted.entrySet()){ + + System.out.println("[hashtag=" + entry.getKey() + " , weight=" + entry.getValue() + "]"); + } + + return weights; + } + + /** + * Sort a map by its values + * @param map + * @return + */ + private static > Map + sortByWeight( Map map ) + { + List> list = + new LinkedList>( map.entrySet() ); + Collections.sort( list, new Comparator>() + { + public int compare( Map.Entry o1, Map.Entry o2 ) + { + return (o2.getValue()).compareTo( o1.getValue() ); + } + }); + + Map result = new LinkedHashMap(); + for (Map.Entry entry : list) + { + result.put( entry.getKey(), entry.getValue() ); + } + return result; + } + /** * Indicates whether the scope is the whole infrastructure. * @return true if it is, false otherwise. diff --git a/src/main/java/org/gcube/portlets/user/topics/shared/HashTagAndOccurrence.java b/src/main/java/org/gcube/portlets/user/topics/shared/HashTagAndOccurrence.java index 5315f7a..05f8003 100644 --- a/src/main/java/org/gcube/portlets/user/topics/shared/HashTagAndOccurrence.java +++ b/src/main/java/org/gcube/portlets/user/topics/shared/HashTagAndOccurrence.java @@ -3,11 +3,18 @@ package org.gcube.portlets.user.topics.shared; public class HashTagAndOccurrence implements Comparable{ private String hashtag; private Integer occurrence; + private double weight; public HashTagAndOccurrence(String hashtag, Integer occurrence) { super(); this.hashtag = hashtag; this.occurrence = occurrence; } + public HashTagAndOccurrence(String hashtag, Integer occurrence, double weight) { + super(); + this.hashtag = hashtag; + this.occurrence = occurrence; + this.weight = weight; + } public String getHashtag() { return hashtag; } @@ -20,16 +27,20 @@ public class HashTagAndOccurrence implements Comparable{ public void setOccurrence(Integer occurrence) { this.occurrence = occurrence; } + public double getWeight() { + return weight; + } + public void setWeight(double weight) { + this.weight = weight; + } + @Override public String toString() { return "HashTagAndOccurrence [hashtag=" + hashtag + ", occurrence=" - + occurrence + "]"; + + occurrence + ", weight=" + weight + "]"; } @Override public int compareTo(HashTagAndOccurrence o) { - if (this.occurrence == o.getOccurrence()) return 0; - return (this.occurrence > o.getOccurrence()) ? 1 : -1; + return Double.compare(o.getWeight(), this.weight); } - - } diff --git a/src/main/java/org/gcube/portlets/user/topics/shared/HashtagsWrapper.java b/src/main/java/org/gcube/portlets/user/topics/shared/HashtagsWrapper.java index 63ed75f..f318381 100644 --- a/src/main/java/org/gcube/portlets/user/topics/shared/HashtagsWrapper.java +++ b/src/main/java/org/gcube/portlets/user/topics/shared/HashtagsWrapper.java @@ -2,8 +2,14 @@ package org.gcube.portlets.user.topics.shared; import java.io.Serializable; import java.util.ArrayList; - +/** + * @author Massimiliano Assante at ISTI-CNR + * (massimiliano.assante@isti.cnr.it) + * @author Costantino Perciante at ISTI-CNR + * (costantino.perciante@isti.cnr.it) + */ public class HashtagsWrapper implements Serializable { + private static final long serialVersionUID = -532083077958376460L; private boolean isInfrastructure; private ArrayList hashtags; public HashtagsWrapper(boolean isInfrastructure, ArrayList hashtags) { diff --git a/src/main/resources/org/gcube/portlets/user/topics/TopTopics.gwt.xml b/src/main/resources/org/gcube/portlets/user/topics/TopTopics.gwt.xml index a75122d..d422720 100644 --- a/src/main/resources/org/gcube/portlets/user/topics/TopTopics.gwt.xml +++ b/src/main/resources/org/gcube/portlets/user/topics/TopTopics.gwt.xml @@ -3,13 +3,15 @@ - - + + + + + - + diff --git a/src/main/webapp/TopTopics.css b/src/main/webapp/TopTopics.css index 6027732..3a4dde2 100644 --- a/src/main/webapp/TopTopics.css +++ b/src/main/webapp/TopTopics.css @@ -6,7 +6,6 @@ a.topiclink, a.topiclink:active, a.topiclink:visited { cursor: hand; text-decoration: none; color: #0078b2 !important; - font-weight: bold !important; } a.topiclink:hover {