/** * Copyright (c) 2008-2012 The Sakai Foundation * * Licensed under the Educational Community 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.osedu.org/licenses/ECL-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.sakaiproject.profile2.job; import java.math.BigDecimal; import java.math.RoundingMode; import java.util.HashMap; import java.util.List; import java.util.Map; import lombok.Setter; import org.apache.commons.lang.StringUtils; import org.apache.log4j.Logger; import org.quartz.JobExecutionContext; import org.quartz.JobExecutionException; import org.quartz.SchedulerException; import org.quartz.StatefulJob; import org.sakaiproject.profile2.logic.ProfileConnectionsLogic; import org.sakaiproject.profile2.logic.ProfileExternalIntegrationLogic; import org.sakaiproject.profile2.logic.ProfileImageLogic; import org.sakaiproject.profile2.logic.ProfileKudosLogic; import org.sakaiproject.profile2.logic.ProfileLogic; import org.sakaiproject.profile2.logic.ProfileMessagingLogic; import org.sakaiproject.profile2.logic.ProfileStatusLogic; import org.sakaiproject.profile2.logic.SakaiProxy; import org.sakaiproject.profile2.model.ExternalIntegrationInfo; import org.sakaiproject.profile2.model.Person; import org.sakaiproject.profile2.model.ProfileImage; import org.sakaiproject.profile2.model.ProfilePrivacy; import org.sakaiproject.profile2.model.UserProfile; import org.sakaiproject.profile2.util.ProfileConstants; import org.sakaiproject.tool.api.Session; import org.sakaiproject.tool.api.SessionManager; /** * This is the Kudos calculation job. * * <p>Certain items/events have weightings, these are calculated and summed to give a score. * <br />That score is then divided by the total number of possible items/weightings and converted to a percentage * to give the total kudos ranking for the user. * </p> * * @author Steve Swinsburg (steve.swinsburg@gmail.com) * */ public class KudosJob implements StatefulJob { private static final Logger log = Logger.getLogger(KudosJob.class); private final String BEAN_ID = "org.sakaiproject.profile2.job.KudosJob"; /** * setup the rule map */ private final HashMap<String,BigDecimal> RULES = new HashMap<String,BigDecimal>() { private static final long serialVersionUID = 1L; { //points for profile completeness put("nickname", new BigDecimal("1")); put("birthday", new BigDecimal("0.5")); put("email", new BigDecimal("1")); put("homePage", new BigDecimal("1")); put("workPhone", new BigDecimal("1")); put("homePhone", new BigDecimal("1")); put("mobilePhone", new BigDecimal("1")); put("position", new BigDecimal("0.5")); put("department", new BigDecimal("0.5")); put("school", new BigDecimal("0.5")); put("room", new BigDecimal("0.5")); put("course", new BigDecimal("0.5")); put("subjects", new BigDecimal("0.5")); put("favouriteBooks", new BigDecimal("0.25")); put("favouriteTvShows", new BigDecimal("0.25")); put("favouriteMovies", new BigDecimal("0.25")); put("favouriteQuotes", new BigDecimal("0.25")); put("personalSummary", new BigDecimal("2")); //points for openness in privacy put("profileImageShared", new BigDecimal("0.05")); put("profileImageBonus", new BigDecimal("0.05")); put("basicInfoShared", new BigDecimal("0.05")); put("basicInfoBonus", new BigDecimal("0.05")); put("contactInfoShared", new BigDecimal("0.05")); put("contactInfoBonus", new BigDecimal("0.05")); put("personalInfoShared", new BigDecimal("0.05")); put("personalInfoBonus", new BigDecimal("0.05")); put("staffInfoShared", new BigDecimal("0.05")); put("staffInfoBonus", new BigDecimal("0.05")); put("studentInfoShared", new BigDecimal("0.05")); put("studentInfoBonus", new BigDecimal("0.05")); put("viewConnectionsShared", new BigDecimal("0.05")); put("viewConnectionsBonus", new BigDecimal("0.05")); put("viewStatusShared", new BigDecimal("0.05")); put("viewStatusBonus", new BigDecimal("0.05")); put("viewPicturesShared", new BigDecimal("0.05")); put("viewPicturesBonus", new BigDecimal("0.05")); put("showBirthYear", new BigDecimal("0.1")); //points for usage - more points for the heavier usage put("hasImage", new BigDecimal("5")); put("hasOneConnection", new BigDecimal("2")); put("hasMoreThanTenConnections", new BigDecimal("3")); put("hasOneSentMessage", new BigDecimal("2")); put("hasMoreThanTenSentMessages", new BigDecimal("3")); put("hasOneStatusUpdate", new BigDecimal("0.25")); // add when PRFL-191 is added //put("hasMoreThanTenStatusUpdates", new BigDecimal(1)); //put("hasMoreThanOneHundredStatusUpdates", new BigDecimal(2)); put("twitterEnabled", new BigDecimal("2")); put("hasOneGalleryPicture", new BigDecimal("0.25")); put("hasMoreThanTenGalleryPictures", new BigDecimal("1")); //points for others viewing their profile, not yet implemented //put("hasMoreThanOneVisitor", new BigDecimal(0.05)); //put("hasMoreThanTenUniqueVisitors", new BigDecimal(2)); //put("hasMoreThanOneHundredUniqueVisitors", new BigDecimal(3)); } }; /** * Calculate the score for this person * @param person Person object * @return */ private BigDecimal getScore(Person person) { BigDecimal score = new BigDecimal(0); //profile UserProfile profile = person.getProfile(); if(profile != null){ //basic if(nb(profile.getNickname())){ score = score.add(val("nickname")); } if(nb(profile.getBirthday())){ score = score.add(val("birthday")); } //contact if(nb(profile.getEmail())){ score = score.add(val("email")); } if(nb(profile.getHomepage())){ score = score.add(val("homePage")); } if(nb(profile.getWorkphone())){ score = score.add(val("workPhone")); } if(nb(profile.getHomephone())){ score = score.add(val("homePhone")); } if(nb(profile.getMobilephone())){ score = score.add(val("mobilePhone")); } //staff/student if(nb(profile.getPosition())){ score = score.add(val("position")); } if(nb(profile.getDepartment())){ score = score.add(val("department")); } if(nb(profile.getSchool())){ score = score.add(val("school")); } if(nb(profile.getRoom())){ score = score.add(val("room")); } if(nb(profile.getCourse())){ score = score.add(val("course")); } if(nb(profile.getSubjects())){ score = score.add(val("subjects")); } //personal if(nb(profile.getFavouriteBooks())){ score = score.add(val("favouriteBooks")); } if(nb(profile.getFavouriteTvShows())){ score = score.add(val("favouriteTvShows")); } if(nb(profile.getFavouriteMovies())){ score = score.add(val("favouriteMovies")); } if(nb(profile.getFavouriteQuotes())){ score = score.add(val("favouriteQuotes")); } if(nb(profile.getPersonalSummary())){ score = score.add(val("personalSummary")); } } ProfilePrivacy privacy = person.getPrivacy(); if(privacy != null){ //profile image switch(privacy.getProfileImage()) { case (ProfileConstants.PRIVACY_OPTION_ONLYFRIENDS) : score = score.add(val("profileImageShared")); break; case (ProfileConstants.PRIVACY_OPTION_EVERYONE) : score = score.add(val("profileImageShared")); score = score.add(val("profileImageBonus")); break; } //basic info switch(privacy.getBasicInfo()) { case (ProfileConstants.PRIVACY_OPTION_ONLYFRIENDS) : score = score.add(val("basicInfoShared")); break; case (ProfileConstants.PRIVACY_OPTION_EVERYONE) : score = score.add(val("basicInfoShared")); score = score.add(val("basicInfoBonus")); break; } //contact info switch(privacy.getContactInfo()) { case (ProfileConstants.PRIVACY_OPTION_ONLYFRIENDS) : score = score.add(val("contactInfoShared")); break; case (ProfileConstants.PRIVACY_OPTION_EVERYONE) : score = score.add(val("contactInfoShared")); score = score.add(val("contactInfoBonus")); break; } //personal info switch(privacy.getPersonalInfo()) { case (ProfileConstants.PRIVACY_OPTION_ONLYFRIENDS) : score = score.add(val("personalInfoShared")); break; case (ProfileConstants.PRIVACY_OPTION_EVERYONE) : score = score.add(val("personalInfoShared")); score = score.add(val("personalInfoBonus")); break; } //staff info switch(privacy.getStaffInfo()) { case (ProfileConstants.PRIVACY_OPTION_ONLYFRIENDS) : score = score.add(val("staffInfoShared")); break; case (ProfileConstants.PRIVACY_OPTION_EVERYONE) : score = score.add(val("staffInfoShared")); score = score.add(val("staffInfoBonus")); break; } //student info switch(privacy.getStudentInfo()) { case (ProfileConstants.PRIVACY_OPTION_ONLYFRIENDS) : score = score.add(val("studentInfoShared")); break; case (ProfileConstants.PRIVACY_OPTION_EVERYONE) : score = score.add(val("studentInfoShared")); score = score.add(val("studentInfoBonus")); break; } //view connections switch(privacy.getMyFriends()) { case (ProfileConstants.PRIVACY_OPTION_ONLYFRIENDS) : score = score.add(val("viewConnectionsShared")); break; case (ProfileConstants.PRIVACY_OPTION_EVERYONE) : score = score.add(val("viewConnectionsShared")); score = score.add(val("viewConnectionsBonus")); break; } //view status switch(privacy.getMyStatus()) { case (ProfileConstants.PRIVACY_OPTION_ONLYFRIENDS) : score = score.add(val("viewStatusShared")); break; case (ProfileConstants.PRIVACY_OPTION_EVERYONE) : score = score.add(val("viewStatusShared")); score = score.add(val("viewStatusBonus")); break; } //view pictures. if it's disabled, assign full points if(sakaiProxy.isProfileGalleryEnabledGlobally()) { switch(privacy.getMyPictures()) { case (ProfileConstants.PRIVACY_OPTION_ONLYFRIENDS) : score = score.add(val("viewPicturesShared")); break; case (ProfileConstants.PRIVACY_OPTION_EVERYONE) : score = score.add(val("viewPicturesShared")); score = score.add(val("viewPicturesBonus")); break; } } else { score = score.add(val("viewPicturesShared")); score = score.add(val("viewPicturesBonus")); } //birth year visible if(privacy.isShowBirthYear()){ score = score.add(val("showBirthYear")); } } //points for image that isn't the default ProfileImage image = imageLogic.getProfileImage(person, ProfileConstants.PROFILE_IMAGE_MAIN); if(image != null){ if(image.getBinary() != null) { score = score.add(val("hasImage")); } if(!StringUtils.equals(image.getUrl(), imageLogic.getUnavailableImageURL())) { score = score.add(val("hasImage")); } } //number of connections int numConnections = connectionsLogic.getConnectionsForUserCount(person.getUuid()); if(numConnections >= 1){ score = score.add(val("hasOneConnection")); } if(numConnections > 10){ score = score.add(val("hasMoreThanTenConnections")); } //number of sent messages int numSentMessages = messagingLogic.getSentMessagesCount(person.getUuid()); if(numSentMessages >= 1){ score = score.add(val("hasOneSentMessage")); } if(numSentMessages > 10){ score = score.add(val("hasMoreThanTenSentMessages")); } //number of status updates int numStatusUpdates = statusLogic.getStatusUpdatesCount(person.getUuid()); if(numStatusUpdates >= 1) { score = score.add(val("hasOneStatusUpdate")); } /* enable for PRFL-191 as well as entries in map above. if(numStatusUpdates > 10) { score = score.add(val("hasMoreThanTenStatusUpdates")); } if(numStatusUpdates > 100) { score = score.add(val("hasMoreThanOneHundredStatusUpdates")); } */ /* ProfilePreferences prefs = person.getPreferences(); if(prefs != null){ //is twitter enabled? if(prefs.isTwitterEnabled()) { score = score.add(val("twitterEnabled")); } } */ ExternalIntegrationInfo externalIntegrationInfo = externalIntegrationLogic.getExternalIntegrationInfo(person.getUuid()); if(externalIntegrationInfo != null){ if(externalIntegrationInfo.isTwitterAlreadyConfigured()) { score = score.add(val("twitterEnabled")); } } //if gallery enabled, number of gallery pictures if(sakaiProxy.isProfileGalleryEnabledGlobally()){ int numGalleryPictures = imageLogic.getGalleryImagesCount(person.getUuid()); if(numGalleryPictures >= 1) { score = score.add(val("hasOneGalleryPicture")); } if(numGalleryPictures > 10){ score = score.add(val("hasMoreThanTenGalleryPictures")); } } return score; } public void execute(JobExecutionContext context) throws JobExecutionException { //abort if already running on THIS server node (cannot check other nodes) try { while(isJobCurrentlyRunning(context)) { String beanId = context.getJobDetail().getJobDataMap().getString(BEAN_ID); log.warn("Another instance of "+beanId+" is currently running - Execution aborted."); return; } } catch(SchedulerException e){ log.error("Aborting job execution due to " +e.toString(), e); return; } log.info("KudosJob run"); //start a session for admin so we can get full profiles Session session = sessionManager.startSession(); sessionManager.setCurrentSession(session); session.setUserEid("admin"); session.setUserId("admin"); //get total possible score BigDecimal total = getTotal(); log.info("Total score possible: " + total.setScale(2, RoundingMode.HALF_UP)); //get total number of records List<String> profileUuids = profileLogic.getAllSakaiPersonIds(); //iterate over list getting a chunk of profiles at a time for(String userUuid: profileUuids) { Person person = profileLogic.getPerson(userUuid); if(person == null){ continue; } log.info("Processing user: " + userUuid + " (" + person.getDisplayName() + ")"); //get score for user BigDecimal score = getScore(person); BigDecimal percentage = getScoreAsPercentage(score, total); int adjustedScore = getScoreOutOfTen(score, total); //save it if(kudosLogic.updateKudos(userUuid, adjustedScore, percentage)) { log.info("Kudos updated for user: " + userUuid + ", score: " + score.setScale(2, RoundingMode.HALF_UP) + ", percentage: " + percentage + ", adjustedScore: " + adjustedScore); } } session.setUserId(null); session.setUserEid(null); log.info("KudosJob finished"); } /** * Are multiples of this job currently running? * @param context * @return * @throws SchedulerException */ private boolean isJobCurrentlyRunning(JobExecutionContext context) throws SchedulerException { String beanId = context.getJobDetail().getJobDataMap().getString(BEAN_ID); List<JobExecutionContext> jobsRunning = context.getScheduler().getCurrentlyExecutingJobs(); int jobsCount = 0; for(JobExecutionContext j : jobsRunning) if(StringUtils.equals(beanId, j.getJobDetail().getJobDataMap().getString(BEAN_ID))) { jobsCount++; //this job=1, any more and they are multiples. } if(jobsCount > 1) { return true; } return false; } /** * Helper for StringUtils.isNotBlank * @param s1 String to check * @return */ private boolean nb(String s1){ return StringUtils.isNotBlank(s1); } /** * Helper to get the value of the key from the RULES map * @param key key to getvalue for * @return BigDecimal value */ private BigDecimal val(String key){ return RULES.get(key); } /** * Gets the total of all BigDecimals in the RULES map * @param map * @return */ private BigDecimal getTotal() { BigDecimal total = new BigDecimal("0"); if(RULES != null) { for(Map.Entry<String,BigDecimal> entry : RULES.entrySet()) { total = total.add(entry.getValue()); } } return total; } /** * Gets the score as a percentage, two decimal precision * @param score score for user * @param total total possible score * @return */ private BigDecimal getScoreAsPercentage(BigDecimal score, BigDecimal total) { return score.divide(total, 4, RoundingMode.HALF_UP).multiply(new BigDecimal("100")).stripTrailingZeros(); } /** * Gets the score out of ten as an int, and rounded up * @param score score for user * @param total total possible score * @return */ private static int getScoreOutOfTen(BigDecimal score, BigDecimal total) { return score.divide(total, 1, RoundingMode.HALF_UP).multiply(new BigDecimal("10")).intValue(); } public void init(){ log.info("KudosJob.init()"); } @Setter private SakaiProxy sakaiProxy; @Setter private ProfileLogic profileLogic; @Setter private ProfileKudosLogic kudosLogic; @Setter private ProfileImageLogic imageLogic; @Setter private ProfileConnectionsLogic connectionsLogic; @Setter private ProfileMessagingLogic messagingLogic; @Setter private ProfileStatusLogic statusLogic; @Setter private ProfileExternalIntegrationLogic externalIntegrationLogic; @Setter private SessionManager sessionManager; }