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}