001/** 002 * Copyright 2014 Tampere University of Technology, Pori Department 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016package service.tut.pori.twitterjazz; 017 018import java.util.Collection; 019import java.util.EnumSet; 020import java.util.List; 021import java.util.Set; 022 023import org.apache.log4j.Logger; 024import org.quartz.Job; 025import org.quartz.JobBuilder; 026import org.quartz.JobDataMap; 027import org.quartz.JobExecutionContext; 028import org.quartz.JobExecutionException; 029import org.springframework.context.ApplicationListener; 030 031import service.tut.pori.contentanalysis.AnalysisBackend; 032import service.tut.pori.contentanalysis.AnalysisBackend.Capability; 033import service.tut.pori.contentanalysis.AsyncTask.TaskStatus; 034import service.tut.pori.contentanalysis.BackendDAO; 035import service.tut.pori.contentanalysis.BackendStatusList; 036import service.tut.pori.contentanalysis.CAContentCore; 037import service.tut.pori.contentanalysis.CAContentCore.ServiceType; 038import service.tut.pori.contentanalysis.MediaObject; 039import service.tut.pori.contentanalysis.MediaObjectDAO; 040import service.tut.pori.contentanalysis.MediaObjectList; 041import service.tut.pori.contentanalysis.PhotoDAO; 042import service.tut.pori.twitterjazz.TwitterExtractor.ContentType; 043import service.tut.pori.users.twitter.TwitterProperties; 044import service.tut.pori.users.twitter.TwitterUserCore; 045import twitter4j.TwitterFactory; 046import twitter4j.conf.Configuration; 047import twitter4j.conf.ConfigurationBuilder; 048import core.tut.pori.context.ServiceInitializer; 049import core.tut.pori.http.parameters.DataGroups; 050import core.tut.pori.http.parameters.Limits; 051import core.tut.pori.http.parameters.SortOptions; 052import core.tut.pori.users.UserEvent; 053import core.tut.pori.users.UserEvent.EventType; 054import core.tut.pori.users.UserIdentity; 055import core.tut.pori.utils.MediaUrlValidator.MediaType; 056 057/** 058 * TwitterJazz core methods. 059 * 060 */ 061public final class TJContentCore { 062 /** default capabilities for Twitter tasks */ 063 public static final EnumSet<Capability> DEFAULT_CAPABILITIES = EnumSet.of(Capability.TWITTER_SUMMARIZATION, Capability.PHOTO_ANALYSIS, Capability.BACKEND_FEEDBACK); 064 private static final Logger LOGGER = Logger.getLogger(TJContentCore.class); 065 private static final String JOB_KEY_USER_ID = "userId"; 066 private static final EnumSet<MediaType> MEDIA_TYPES_TJ = EnumSet.allOf(MediaType.class); 067 private static final EnumSet<ServiceType> SERVICE_TYPES_TJ = EnumSet.of(ServiceType.TWITTER_JAZZ); 068 private static TwitterFactory TWITTER_FACTORY = null; 069 070 /** 071 * 072 */ 073 private TJContentCore(){ 074 // nothing needed 075 } 076 077 /** 078 * 079 * @param response 080 * @throws IllegalArgumentException 081 */ 082 public static void taskFinished(TwitterTaskResponse response) throws IllegalArgumentException { 083 CAContentCore.validateTaskResponse(response); 084 085 LOGGER.debug("TaskId: "+response.getTaskId()+", backendId: "+response.getBackendId()); 086 087 switch(response.getTaskType()){ 088 case TWITTER_PROFILE_SUMMARIZATION: 089 TwitterSummarizationTask.taskFinished(response); 090 break; 091 default: 092 throw new IllegalArgumentException("Unsupported "+service.tut.pori.contentanalysis.Definitions.ELEMENT_TASK_TYPE+": "+response.getTaskType().name()); 093 } 094 } 095 096 /** 097 * 098 * @param authenticatedUser 099 * @param dataGroups 100 * @param limits 101 * @param sortOptions 102 * @return list of media objects or null if none was found 103 */ 104 public static MediaObjectList retrieveTagsForUser(UserIdentity authenticatedUser, DataGroups dataGroups, Limits limits, SortOptions sortOptions) { 105 return ServiceInitializer.getDAOHandler().getSolrDAO(MediaObjectDAO.class).search(authenticatedUser, dataGroups, limits, MEDIA_TYPES_TJ, SERVICE_TYPES_TJ, sortOptions, null, null); 106 } 107 108 /** 109 * Create and schedule twitter summarization task with the given details. If details have taskId given, the task will not be re-added, and will simply be (re-)scheduled. 110 * 111 * If the details contains no back-ends, default back-ends will be added. See {@link #DEFAULT_CAPABILITIES} 112 * 113 * @param details details of the task, if profile object is given, it will be ignored. 114 * @return the id of the generated task or null on failure 115 */ 116 public static Long summarize(TwitterSummarizationTaskDetails details) { 117 Long taskId = details.getTaskId(); 118 if(taskId != null){ 119 LOGGER.debug("Task id was given, will not add task."); 120 }else{ 121 BackendStatusList backends = details.getBackends(); 122 if(BackendStatusList.isEmpty(backends)){ 123 LOGGER.debug("No back-ends given, using defaults..."); 124 List<AnalysisBackend> ends = ServiceInitializer.getDAOHandler().getSQLDAO(BackendDAO.class).getBackends(DEFAULT_CAPABILITIES); 125 if(ends == null){ 126 LOGGER.warn("Aborting task, no capable back-ends."); 127 return null; 128 } 129 130 backends = new BackendStatusList(); 131 backends.setBackendStatus(ends, TaskStatus.NOT_STARTED); 132 details.setBackends(backends); 133 } 134 135 taskId = ServiceInitializer.getDAOHandler().getSQLDAO(TwitterTaskDAO.class).insertTask(details); 136 if(taskId == null){ 137 LOGGER.error("Task schedule failed: failed to insert new task."); 138 return null; 139 } 140 } 141 142 JobDataMap data = new JobDataMap(); 143 TwitterSummarizationTask.setTaskId(data, taskId); 144 LOGGER.debug("Scheduling task, id: "+taskId); 145 JobBuilder builder = JobBuilder.newJob(TwitterSummarizationTask.class); 146 builder.setJobData(data); 147 if(CAContentCore.schedule(builder)){ 148 return taskId; 149 }else{ 150 LOGGER.warn("Failed to schedule task, id: "+taskId); 151 return null; 152 } 153 } 154 155 /** 156 * Create and schedule twitter summarization task(s) with the given details. A separate task is created for each of the given screen names. If no screen names are given, task is generated for the authenticated user's twitter account. 157 * 158 * @param authenticatedUser 159 * @param contentTypes 160 * @param screenNames 161 * @param summarize 162 * @param synchronize 163 */ 164 public static void summarize(UserIdentity authenticatedUser, Set<ContentType> contentTypes, Collection<String> screenNames, boolean summarize, boolean synchronize){ 165 if(!UserIdentity.isValid(authenticatedUser)){ 166 LOGGER.warn("Invalid user."); 167 return; 168 } 169 if((!summarize && !synchronize) || (contentTypes == null || contentTypes.isEmpty())){ 170 LOGGER.warn("Ignored no-op task: no summarize or synchronize requested, or no content types."); 171 return; 172 } 173 174 TwitterSummarizationTaskDetails details = new TwitterSummarizationTaskDetails(); 175 details.setUserId(authenticatedUser); 176 details.setContentTypes(contentTypes); 177 details.setSummarize(summarize); 178 details.setSynchronize(synchronize); 179 180 if(screenNames == null || screenNames.isEmpty()){ 181 LOGGER.debug("No screen names."); 182 TJContentCore.summarize(details); 183 }else{ 184 LOGGER.debug("Screen names given, generating "+screenNames.size()+" tasks."); 185 for(String screenName : screenNames){ 186 details.setScreenName(screenName); 187 TJContentCore.summarize(details); 188 } 189 } 190 } 191 192 /** 193 * 194 * @param authenticatedUser 195 * @param rankedObjects 196 * @throws NumberFormatException 197 * @throws IllegalArgumentException 198 */ 199 public static void setRanks(UserIdentity authenticatedUser, MediaObjectList rankedObjects) { 200 if(!MediaObjectList.isValid(rankedObjects)){ 201 throw new IllegalArgumentException("Invalid media object list."); 202 } 203 204 if(!UserIdentity.isValid(authenticatedUser)){ 205 LOGGER.warn("Invalid user."); 206 return; 207 } 208 209 List<MediaObject> objects = rankedObjects.getMediaObjects(); 210 for(MediaObject object : objects){ 211 String mediaObjectId = object.getMediaObjectId(); 212 if(!UserIdentity.equals(authenticatedUser, object.getOwnerUserId())){ 213 LOGGER.debug("User ids do not match for media object id: "+mediaObjectId+", user, id: "+authenticatedUser.getUserId()); 214 throw new IllegalArgumentException("Bad media object id."); 215 } 216 } 217 218 if(!ServiceInitializer.getDAOHandler().getSolrDAO(PhotoDAO.class).update(rankedObjects)){ 219 LOGGER.warn("Failed to update media objects."); 220 } 221 } 222 223 /** 224 * 225 * @return thread-safe factory instance for the twitter properties 226 */ 227 protected static TwitterFactory getTwitterFactory(){ 228 TwitterFactory _factory = TWITTER_FACTORY; 229 if(_factory == null){ 230 synchronized (TwitterExtractor.class) { 231 if(TWITTER_FACTORY != null){ 232 _factory = TWITTER_FACTORY; 233 }else{ 234 TwitterProperties tp = ServiceInitializer.getPropertyHandler().getSystemProperties(TwitterProperties.class); 235 LOGGER.debug("Initializing a new twitter factory..."); 236 boolean debug = tp.isDebugEnabled(); 237 if(debug){ 238 LOGGER.debug("Debug enabled."); 239 } 240 241 Configuration configuration = new ConfigurationBuilder() 242 .setDebugEnabled(debug) 243 .setIncludeEntitiesEnabled(true) 244 .setOAuthConsumerKey(tp.getApiKey()) 245 .setOAuthConsumerSecret(tp.getClientSecret()) 246 .build(); 247 _factory = TWITTER_FACTORY = new TwitterFactory(configuration); 248 } 249 } // synchronized 250 } // if 251 return _factory; 252 } 253 254 /** 255 * Listener for user related events. 256 * 257 * Automatically instantiated by Spring as a bean. 258 */ 259 @SuppressWarnings("unused") 260 private static class UserEventListener implements ApplicationListener<UserEvent>{ 261 262 @Override 263 public void onApplicationEvent(UserEvent event) { 264 if(event.getType() == EventType.USER_AUTHORIZATION_REVOKED && event.getSource().equals(TwitterUserCore.class)){ 265 Long userId = event.getUserId().getUserId(); 266 LOGGER.debug("Detected event of type "+EventType.USER_AUTHORIZATION_REVOKED.name()+", scheduling removal of weight modifiers for user, id: "+userId); 267 268 JobDataMap data = new JobDataMap(); 269 data.put(JOB_KEY_USER_ID, userId); 270 JobBuilder builder = JobBuilder.newJob(TagRemovalJob.class); 271 builder.setJobData(data); 272 CAContentCore.schedule(builder); 273 } 274 } 275 } // class UserEventListener 276 277 /** 278 * A job for removing all user content generated by TwitterJazz service. 279 * 280 */ 281 public static class TagRemovalJob implements Job{ 282 283 @Override 284 public void execute(JobExecutionContext context) throws JobExecutionException { 285 JobDataMap data = context.getMergedJobDataMap(); 286 Long userId = data.getLong(JOB_KEY_USER_ID); 287 LOGGER.debug("Removing all content for user, id: "+userId); 288 MediaObjectDAO vDAO = ServiceInitializer.getDAOHandler().getSolrDAO(MediaObjectDAO.class); 289 MediaObjectList mediaObjects = vDAO.search(new UserIdentity(userId), null, null, MEDIA_TYPES_TJ, SERVICE_TYPES_TJ, null , new long[]{userId}, null); 290 if(MediaObjectList.isEmpty(mediaObjects)){ 291 LOGGER.debug("User, id: "+userId+" has no media objects."); 292 return; 293 } 294 List<String> remove = mediaObjects.getMediaObjectIds(); 295 if(!vDAO.remove(remove)){ 296 LOGGER.debug("Failed to remove objects for user, id: "+userId); 297 } 298 } 299 } // class TagRemovalJob 300}