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.facebookjazz; 017 018import java.util.EnumSet; 019import java.util.HashMap; 020import java.util.HashSet; 021import java.util.Iterator; 022import java.util.List; 023import java.util.Map.Entry; 024 025import org.apache.log4j.Logger; 026import org.quartz.Job; 027import org.quartz.JobBuilder; 028import org.quartz.JobDataMap; 029import org.quartz.JobExecutionContext; 030import org.quartz.JobExecutionException; 031import org.springframework.context.ApplicationListener; 032 033import service.tut.pori.contentanalysis.AsyncTask; 034import service.tut.pori.contentanalysis.AnalysisBackend.Capability; 035import service.tut.pori.contentanalysis.AsyncTask.TaskStatus; 036import service.tut.pori.contentanalysis.AsyncTask.TaskType; 037import service.tut.pori.contentanalysis.CAContentCore; 038import service.tut.pori.contentanalysis.CAContentCore.ServiceType; 039import service.tut.pori.contentanalysis.AnalysisBackend; 040import service.tut.pori.contentanalysis.BackendDAO; 041import service.tut.pori.contentanalysis.BackendStatusList; 042import service.tut.pori.contentanalysis.MediaObject; 043import service.tut.pori.contentanalysis.MediaObjectDAO; 044import service.tut.pori.contentanalysis.MediaObjectList; 045import service.tut.pori.contentanalysis.PhotoDAO; 046import service.tut.pori.users.facebook.FacebookUserCore; 047import core.tut.pori.context.ServiceInitializer; 048import core.tut.pori.http.parameters.DataGroups; 049import core.tut.pori.http.parameters.Limits; 050import core.tut.pori.http.parameters.SortOptions; 051import core.tut.pori.users.UserEvent; 052import core.tut.pori.users.UserEvent.EventType; 053import core.tut.pori.users.UserIdentity; 054import core.tut.pori.utils.MediaUrlValidator.MediaType; 055 056 057/** 058 * FacebookJazz core methods. 059 */ 060public final class FBJContentCore { 061 /** default capabilities for Facebook tasks */ 062 public static final EnumSet<Capability> DEFAULT_CAPABILITIES = EnumSet.of(Capability.FACEBOOK_SUMMARIZATION, Capability.PHOTO_ANALYSIS, Capability.BACKEND_FEEDBACK); 063 private static final DataGroups DATA_GROUP_ALL = new DataGroups(DataGroups.DATA_GROUP_ALL); 064 private static final String JOB_KEY_USER_ID = "userId"; 065 private static final Logger LOGGER = Logger.getLogger(FBJContentCore.class); 066 private static final EnumSet<MediaType> MEDIA_TYPES_FBJ = EnumSet.allOf(MediaType.class); 067 private static final EnumSet<ServiceType> SERVICE_TYPES_FBJ = EnumSet.of(ServiceType.FACEBOOK_JAZZ); 068 069 /** 070 * 071 */ 072 private FBJContentCore(){ 073 // nothing needed 074 } 075 076 /** 077 * 078 * @param authenticatedUser 079 * @param dataGroups 080 * @param limits 081 * @param sortOptions 082 * @return list of media objects or null if none was found 083 */ 084 public static MediaObjectList retrieveTagsForUser(UserIdentity authenticatedUser, DataGroups dataGroups, Limits limits, SortOptions sortOptions) { 085 return ServiceInitializer.getDAOHandler().getSolrDAO(MediaObjectDAO.class).search(authenticatedUser, dataGroups, limits, MEDIA_TYPES_FBJ, SERVICE_TYPES_FBJ, sortOptions, null, null); 086 } 087 088 /** 089 * Parses the ranks into a {@link service.tut.pori.contentanalysis.MediaObjectList}. Non-existing media object ids will be ignored. 090 * 091 * @param ranks list of rank strings with {@value core.tut.pori.http.Definitions#SEPARATOR_URI_QUERY_TYPE_VALUE} as separator between media object id and rank value 092 * @return null if ranks is null or did not contain valid ranks 093 * @throws IllegalArgumentException on invalid rank string 094 */ 095 public static MediaObjectList parseRankStrings(List<String> ranks) throws IllegalArgumentException { 096 if(ranks == null || ranks.isEmpty()){ 097 LOGGER.debug("No ranks given."); 098 return null; 099 } 100 101 HashMap<String, Integer> rankMap = new HashMap<>(ranks.size()); // mediaObjectId, rank map 102 103 for(String r : ranks){ 104 String[] parts = r.split(core.tut.pori.http.Definitions.SEPARATOR_URI_QUERY_TYPE_VALUE); 105 if(parts.length != 2){ 106 throw new IllegalArgumentException("Failed to process rank parameter: "+r); 107 } 108 109 rankMap.put(parts[0], Integer.valueOf(parts[1])); // let it throw 110 } // for 111 112 if(rankMap.isEmpty()){ 113 LOGGER.debug("No ranks found."); 114 return null; 115 } 116 117 MediaObjectDAO vdao = ServiceInitializer.getDAOHandler().getSolrDAO(MediaObjectDAO.class); 118 MediaObjectList objects = vdao.getMediaObjects(DATA_GROUP_ALL, null, EnumSet.allOf(MediaType.class), null, rankMap.keySet(), null); 119 if(MediaObjectList.isEmpty(objects)){ 120 LOGGER.debug("No objects found."); 121 return null; 122 } 123 124 for(Iterator<Entry<String, Integer>> iter = rankMap.entrySet().iterator(); iter.hasNext();){ // update ranks, remove non-existing 125 Entry<String, Integer> e = iter.next(); 126 String mediaObjectId = e .getKey(); 127 MediaObject object = objects.getMediaObject(mediaObjectId); 128 if(object == null){ 129 LOGGER.debug("Ignored non-existing media object, id: "+mediaObjectId); 130 iter.remove(); 131 }else{ 132 object.setRank(e.getValue()); 133 } 134 } // for 135 136 return (MediaObjectList.isEmpty(objects) ? null : objects); 137 } 138 139 /** 140 * 141 * @param authenticatedUser 142 * @param rankedObjects 143 * @throws NumberFormatException 144 * @throws IllegalArgumentException 145 */ 146 public static void setRanks(UserIdentity authenticatedUser, MediaObjectList rankedObjects) throws NumberFormatException, IllegalArgumentException{ 147 if(!MediaObjectList.isValid(rankedObjects)){ 148 throw new IllegalArgumentException("Invalid media object list."); 149 } 150 151 if(!UserIdentity.isValid(authenticatedUser)){ 152 LOGGER.warn("Invalid user."); 153 return; 154 } 155 156 List<MediaObject> objects = rankedObjects.getMediaObjects(); 157 HashSet<String> voids = new HashSet<>(objects.size()); 158 for(MediaObject object : objects){ 159 String mediaObjectId = object.getMediaObjectId(); 160 if(!UserIdentity.equals(authenticatedUser, object.getOwnerUserId())){ 161 LOGGER.debug("User ids do not match for media object id: "+mediaObjectId+", user, id: "+authenticatedUser.getUserId()); 162 throw new IllegalArgumentException("Bad media object id."); 163 } 164 voids.add(mediaObjectId); 165 } 166 167 if(!ServiceInitializer.getDAOHandler().getSolrDAO(PhotoDAO.class).update(rankedObjects)){ 168 LOGGER.warn("Failed to update media objects."); 169 } 170 171 FBFeedbackTaskDetails details = new FBFeedbackTaskDetails(); 172 details.setUserId(authenticatedUser); 173 details.setTags(rankedObjects); 174 scheduleTask(details); 175 } 176 177 /** 178 * 179 * @param details 180 * @return the task id of the generated task or null on failure 181 * @throws UnsupportedOperationException on unsupported task type 182 * @throws IllegalArgumentException on failed schedule 183 */ 184 public static Long scheduleTask(FBFeedbackTaskDetails details) throws UnsupportedOperationException { 185 if(details.getTaskType() != TaskType.FACEBOOK_PROFILE_SUMMARIZATION_FEEDBACK){ 186 throw new UnsupportedOperationException("Unsupported TaskType: "+details.getTaskType().name()); 187 } 188 Long taskId = ServiceInitializer.getDAOHandler().getSQLDAO(FBTaskDAO.class).insertTask(details); 189 if(taskId == null){ 190 LOGGER.error("Task schedule failed: failed to insert new task."); 191 return null; 192 } 193 194 JobDataMap data = new JobDataMap(); 195 AsyncTask.setTaskId(data, taskId); 196 LOGGER.debug("Scheduling task, id: "+taskId); 197 JobBuilder builder = JobBuilder.newJob(FBSummarizationFeedbackTask.class); 198 builder.setJobData(data); 199 if(CAContentCore.schedule(builder)){ 200 return taskId; 201 }else{ 202 LOGGER.warn("Failed to schedule task, id: "+taskId); 203 return null; 204 } 205 } 206 207 /** 208 * Create and schedule facebook summarization task with the given details. If details have taskId given, the task will not be re-added, and will simply be (re-)scheduled. 209 * 210 * If the details contains no back-ends, default back-ends will be added. See {@link #DEFAULT_CAPABILITIES} 211 * 212 * @param details details of the task, if profile object is given, it will be ignored. 213 * @return the id of the generated task or null on failure 214 */ 215 public static Long summarize(FBSummarizationTaskDetails details) { 216 Long taskId = details.getTaskId(); 217 if(taskId != null){ 218 LOGGER.debug("Task id was given, will not add task."); 219 }else{ 220 BackendStatusList backends = details.getBackends(); 221 if(BackendStatusList.isEmpty(backends)){ 222 LOGGER.debug("No back-ends given, using defaults..."); 223 List<AnalysisBackend> ends = ServiceInitializer.getDAOHandler().getSQLDAO(BackendDAO.class).getBackends(DEFAULT_CAPABILITIES); 224 if(ends == null){ 225 LOGGER.warn("Aborting task, no capable back-ends."); 226 return null; 227 } 228 229 backends = new BackendStatusList(); 230 backends.setBackendStatus(ends, TaskStatus.NOT_STARTED); 231 details.setBackends(backends); 232 } 233 234 taskId = ServiceInitializer.getDAOHandler().getSQLDAO(FBTaskDAO.class).insertTask(details); 235 if(taskId == null){ 236 LOGGER.error("Task schedule failed: failed to insert new task."); 237 return null; 238 } 239 } 240 241 JobDataMap data = new JobDataMap(); 242 FBSummarizationTask.setTaskId(data, taskId); 243 LOGGER.debug("Scheduling task, id: "+taskId); 244 JobBuilder builder = JobBuilder.newJob(FBSummarizationTask.class); 245 builder.setJobData(data); 246 if(CAContentCore.schedule(builder)){ 247 return taskId; 248 }else{ 249 LOGGER.warn("Failed to schedule task, id: "+taskId); 250 return null; 251 } 252 } 253 254 /** 255 * 256 * @param authenticatedUser 257 * @param userId 258 * @return list of weight modifiers or null if none was found or permission was denied 259 */ 260 public static WeightModifierList retrieveTagWeights(UserIdentity authenticatedUser, Long userId) { 261 if(userId != null){ 262 if(!UserIdentity.equals(authenticatedUser, userId)){ 263 LOGGER.warn("Permission was denied for tag weights of user, id: "+userId); 264 return null; 265 }else{ 266 return ServiceInitializer.getDAOHandler().getSQLDAO(FacebookJazzDAO.class).getWeightModifiers(new UserIdentity(userId)); 267 } 268 }else{ // no filter, return defaults 269 return ServiceInitializer.getDAOHandler().getSQLDAO(FacebookJazzDAO.class).getWeightModifiers(null); 270 } 271 } 272 273 /** 274 * 275 * @param userIdentity 276 * @param weightModifierList 277 */ 278 public static void setTagWeights(UserIdentity userIdentity, WeightModifierList weightModifierList) { 279 if(!WeightModifierList.isValid(weightModifierList)){ 280 throw new IllegalArgumentException("Invalid "+Definitions.ELEMENT_WEIGHT_MODIFIER_LIST+"."); 281 } 282 ServiceInitializer.getDAOHandler().getSQLDAO(FacebookJazzDAO.class).setWeightModifiers(userIdentity, weightModifierList); 283 } 284 285 /** 286 * 287 * @param response 288 * @throws IllegalArgumentException 289 */ 290 public static void taskFinished(FBTaskResponse response) throws IllegalArgumentException{ 291 CAContentCore.validateTaskResponse(response); 292 293 LOGGER.debug("TaskId: "+response.getTaskId()+", backendId: "+response.getBackendId()); 294 295 switch(response.getTaskType()){ 296 case FACEBOOK_PROFILE_SUMMARIZATION: 297 FBSummarizationTask.taskFinished(response); 298 break; 299 case FACEBOOK_PROFILE_SUMMARIZATION_FEEDBACK: 300 FBSummarizationFeedbackTask.taskFinished(response); 301 break; 302 default: 303 throw new IllegalArgumentException("Unsupported "+service.tut.pori.contentanalysis.Definitions.ELEMENT_TASK_TYPE+": "+response.getTaskType().name()); 304 } 305 } 306 307 /** 308 * Listener for user related events. 309 * 310 * Automatically instantiated by Spring as a bean. 311 */ 312 @SuppressWarnings("unused") 313 private static class UserEventListener implements ApplicationListener<UserEvent>{ 314 315 @Override 316 public void onApplicationEvent(UserEvent event) { 317 if(event.getType() == EventType.USER_AUTHORIZATION_REVOKED && event.getSource().equals(FacebookUserCore.class)){ 318 Long userId = event.getUserId().getUserId(); 319 LOGGER.debug("Detected event of type "+EventType.USER_AUTHORIZATION_REVOKED.name()+", scheduling removal of weight modifiers and tags for user, id: "+userId); 320 321 JobDataMap data = new JobDataMap(); 322 data.put(JOB_KEY_USER_ID, userId); 323 JobBuilder builder = JobBuilder.newJob(WeightModifierRemovalJob.class); 324 builder.setJobData(data); 325 CAContentCore.schedule(builder); 326 327 builder = JobBuilder.newJob(TagRemovalJob.class); 328 builder.setJobData(data); 329 CAContentCore.schedule(builder); 330 } 331 } 332 } // class UserEventListener 333 334 /** 335 * A job for removing all tags of a single user, generated by the Facebook Jazz service. 336 * 337 */ 338 public static class TagRemovalJob implements Job{ 339 340 @Override 341 public void execute(JobExecutionContext context) throws JobExecutionException { 342 JobDataMap data = context.getMergedJobDataMap(); 343 Long userId = data.getLong(JOB_KEY_USER_ID); 344 LOGGER.debug("Removing all content for user, id: "+userId); 345 MediaObjectDAO vDAO = ServiceInitializer.getDAOHandler().getSolrDAO(MediaObjectDAO.class); 346 MediaObjectList mediaObjects = vDAO.search(new UserIdentity(userId), null, null, MEDIA_TYPES_FBJ, SERVICE_TYPES_FBJ, null , new long[]{userId}, null); 347 if(MediaObjectList.isEmpty(mediaObjects)){ 348 LOGGER.debug("No media objects for user, id: "+userId); 349 return; 350 } 351 List<String> remove = mediaObjects.getMediaObjectIds(); 352 if(!vDAO.remove(remove)){ 353 LOGGER.debug("Failed to remove objects for user, id: "+userId); 354 } 355 } 356 } // class TagRemovalJob 357 358 /** 359 * Job for removing content for the user designated by data key JOB_KEY_USER_ID for services designated by data key JOB_KEY_SERVICE_TYPES 360 * 361 */ 362 public static class WeightModifierRemovalJob implements Job{ 363 364 @Override 365 public void execute(JobExecutionContext context) throws JobExecutionException { 366 JobDataMap data = context.getMergedJobDataMap(); 367 Long userId = data.getLong(JOB_KEY_USER_ID); 368 LOGGER.debug("Removing all weight modifiers for user, id: "+userId); 369 ServiceInitializer.getDAOHandler().getSQLDAO(FacebookJazzDAO.class).removeWeightModifers(new UserIdentity(userId)); 370 } 371 } // class MetadataRemovalJob 372}