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.contentanalysis.video;
017
018import java.util.Collection;
019import java.util.EnumSet;
020import java.util.Iterator;
021import java.util.List;
022
023import org.apache.commons.lang3.StringUtils;
024import org.apache.log4j.Logger;
025import org.quartz.JobExecutionContext;
026import org.quartz.JobExecutionException;
027
028import service.tut.pori.contentanalysis.AnalysisBackend;
029import service.tut.pori.contentanalysis.AnalysisBackend.Capability;
030import service.tut.pori.contentanalysis.AsyncTask;
031import service.tut.pori.contentanalysis.BackendStatus;
032import service.tut.pori.contentanalysis.BackendStatusList;
033import service.tut.pori.contentanalysis.MediaObject;
034import service.tut.pori.contentanalysis.MediaObjectDAO;
035import service.tut.pori.contentanalysis.MediaObjectList;
036import core.tut.pori.context.ServiceInitializer;
037import core.tut.pori.users.UserIdentity;
038
039
040/**
041 * An implementation of ASyncTask, meant for executing a feedback task.
042 * 
043 * Requires a valid taskId for execution, provided in a JobExecutionContext.
044 * 
045 */
046public class VideoFeedbackTask extends AsyncTask{
047  private static final Logger LOGGER = Logger.getLogger(VideoFeedbackTask.class);
048  
049  /**
050   * 
051   * @param response
052   * @throws IllegalArgumentException
053   */
054  public static void taskFinished(VideoTaskResponse response) throws IllegalArgumentException{
055    Integer backendId = response.getBackendId();
056    Long taskId = response.getTaskId();
057
058    VideoTaskDAO taskDAO = ServiceInitializer.getDAOHandler().getSQLDAO(VideoTaskDAO.class);
059    BackendStatus taskStatus = taskDAO.getBackendStatus(backendId, taskId);
060    if(taskStatus == null){
061      LOGGER.warn("Backend, id: "+backendId+" returned results for task, not given to the backend. TaskId: "+taskId);
062      throw new IllegalArgumentException("This task is not given for backend, id: "+backendId);
063    }
064
065    TaskStatus status = response.getStatus();
066    if(status == null){
067      LOGGER.warn("Task status not available.");
068      status = TaskStatus.UNKNOWN;
069    }
070    taskStatus.setStatus(status);
071
072    try{
073      VideoList results = response.getVideoList();
074      if(VideoList.isEmpty(results)){
075        LOGGER.warn("No results returned by the backendId: "+backendId);
076        return;
077      }
078
079      if(!VideoList.isValid(results)){
080        LOGGER.warn("Invalid "+Definitions.ELEMENT_VIDEOLIST+".");
081      }
082
083      VideoDAO videoDAO = ServiceInitializer.getDAOHandler().getSolrDAO(VideoDAO.class);
084      if(!videoDAO.setOwners(results)){
085        LOGGER.warn("Could not get owner information for all videos.");
086      }
087
088      VideoList associations = new VideoList();
089      MediaObjectList insert = new MediaObjectList();
090      MediaObjectList update = new MediaObjectList();
091      MediaObjectDAO vdao = ServiceInitializer.getDAOHandler().getSolrDAO(MediaObjectDAO.class);
092      for(Iterator<Video> videoIter = results.getVideos().iterator(); videoIter.hasNext();){
093        Video video = videoIter.next();
094        String guid = video.getGUID();
095        UserIdentity userId = video.getOwnerUserId();
096        if(!UserIdentity.isValid(userId)){  // if this video does not exist, there won't be userId
097          LOGGER.warn("Ignoring non-existing video, GUID: "+guid+" from backend, id: "+backendId);
098          continue;
099        }
100        BackendStatusList c = video.getBackendStatus();
101        if(BackendStatusList.isEmpty(c)){
102          LOGGER.debug("Backend status not available for video, GUID: "+guid);
103        }else if(c.getCombinedStatus() == TaskStatus.ERROR){
104          LOGGER.warn("Error condition detected for video, GUID: "+guid);
105        }else{
106          List<BackendStatus> sList = c.getBackendStatuses();
107          if(sList.size() > 1){
108            status = TaskStatus.ERROR;
109            throw new IllegalArgumentException("Multiple backend statuses.");
110          }
111          if(!backendId.equals(sList.get(0).getBackendId())){
112            status = TaskStatus.ERROR;
113            throw new IllegalArgumentException("Invalid backend status.");
114          }
115        }
116        MediaObjectList vObjects = video.getMediaObjects();
117        if(!MediaObjectList.isEmpty(vObjects)){  // make sure all objects have proper user
118          for(MediaObject mediaObject : vObjects.getMediaObjects()){ // check that the objects are valid
119            if(!backendId.equals(mediaObject.getBackendId())){
120              LOGGER.warn("Task backend id "+backendId+" does not match the backend id "+mediaObject.getBackendId()+" given for media object, objectId: "+mediaObject.getObjectId());
121              mediaObject.setBackendId(backendId);
122            }
123            mediaObject.setOwnerUserId(userId);
124          }
125          vdao.resolveObjectIds(vObjects); // resolve ids for update/insert sort
126          Video iVideo = null;
127          for(MediaObject vo : vObjects.getMediaObjects()){ // re-sort to to updated and new
128            if(StringUtils.isBlank(vo.getMediaObjectId())){ // no media object id, this is a new one
129              if(iVideo == null){
130                associations.getVideo(guid); // get target video for insertion
131                if(iVideo == null){
132                  iVideo = new Video(guid);
133                  associations.addVideo(iVideo);
134                }
135              }       
136              iVideo.addMediaObject(vo);
137              insert.addMediaObject(vo);
138            }else{
139              update.addMediaObject(vo);
140            }
141          } // for
142        } // for objects
143      }
144
145      if(MediaObjectList.isEmpty(insert)){
146        LOGGER.debug("Nothing to insert.");
147      }else if(!MediaObjectList.isValid(insert)){
148        status = TaskStatus.ERROR;
149        throw new IllegalArgumentException("Invalid media object list.");
150      }else if(!videoDAO.insert(insert)){
151        LOGGER.warn("Failed to insert new objects.");   
152      }else{
153        videoDAO.associate(associations);
154      }
155
156      if(MediaObjectList.isEmpty(update)){
157        LOGGER.debug("Nothing to update");
158      }else if(!MediaObjectList.isValid(update)){
159        status = TaskStatus.ERROR;
160        throw new IllegalArgumentException("Invalid media object list.");
161      }else if(!vdao.update(update)){
162        LOGGER.warn("Failed to update objects.");
163      }
164
165      taskDAO.updateMediaStatus(results.getVideos(), taskId);
166      taskDAO.updateTaskStatus(taskStatus, taskId);
167    } finally {
168      ServiceInitializer.getEventHandler().publishEvent(new AsyncTaskEvent(backendId, VideoFeedbackTask.class, status, taskId, TaskType.FEEDBACK));
169    }
170  }
171
172  @Override
173  public void execute(JobExecutionContext context) throws JobExecutionException {
174    executeAddTask(EnumSet.of(Capability.VIDEO_ANALYSIS, Capability.USER_FEEDBACK), ServiceInitializer.getDAOHandler().getSQLDAO(VideoTaskDAO.class), getTaskId(context.getMergedJobDataMap()));
175  }
176  
177  /**
178   * A helper class building VideoTaskDetails usable with {@link VideoFeedbackTask} and executable using {@link  service.tut.pori.contentanalysis.video.VideoContentCore#scheduleTask(VideoTaskDetails)}
179   * @see service.tut.pori.contentanalysis.video.VideoContentCore
180   * @see service.tut.pori.contentanalysis.video.VideoTaskDetails
181   */
182  public static class FeedbackTaskBuilder{
183    private VideoTaskDetails _details = null;
184    
185    /**
186     * 
187     * @param taskType {@link service.tut.pori.contentanalysis.AsyncTask.TaskType#FEEDBACK}
188     * @throws IllegalArgumentException on unsupported/invalid task type
189     */
190    public FeedbackTaskBuilder(TaskType taskType) throws IllegalArgumentException {
191      if(taskType != TaskType.FEEDBACK){
192        throw new IllegalArgumentException("Invalid task type.");
193      }
194      _details = new VideoTaskDetails(taskType);
195    }
196    
197    /**
198     * Add video to feedback task if the given video has (valid) changes
199     * 
200     * @param video
201     * @return this
202     */
203    public FeedbackTaskBuilder addVideo(Video video){
204      if(video == null){
205        LOGGER.warn("Ignored null video.");
206      }else{
207        _details.addVideo(video);
208      }
209      return this;
210    }
211    
212    /**
213     * 
214     * @param videos
215     * @return this
216     */
217    public FeedbackTaskBuilder addVideos(VideoList videos){
218      if(VideoList.isEmpty(videos)){
219        LOGGER.warn("Ignored empty video list.");
220      }else{
221        for(Video p : videos.getVideos()){
222          addVideo(p);
223        }
224      }
225      return this;
226    }
227    
228    /**
229     * 
230     * @param video
231     * @return this
232     * @throws IllegalArgumentException
233     */
234    public FeedbackTaskBuilder addDeletedVideo(Video video) throws IllegalArgumentException{
235      if(video == null){
236        LOGGER.warn("Ignored null video.");
237        return this;
238      }else if(StringUtils.isBlank(video.getGUID())){
239        throw new IllegalArgumentException("No GUID.");
240      }
241      _details.addDeletedVideo(video);
242      return this;
243    }
244    
245    /**
246     * 
247     * @param guids
248     * @return this
249     */
250    public FeedbackTaskBuilder addDeletedVideos(Collection<String> guids){
251      if(guids == null || guids.isEmpty()){
252        LOGGER.warn("Ignored empty deleted video list.");
253        return this;
254      }
255      for(String guid : guids){
256        addDeletedVideo(new Video(guid));
257      }
258      return this;
259    }
260    
261    /**
262     * 
263     * @param videos
264     * @return this
265     */
266    public FeedbackTaskBuilder addDeletedVideos(DeletedVideoList videos){
267      if(DeletedVideoList.isEmpty(videos)){
268        LOGGER.warn("Ignored empty deleted video list.");
269        return this;
270      }
271      DeletedVideoList deleted = _details.getDeletedVideoList();
272      if(DeletedVideoList.isEmpty(deleted)){
273        _details.setDeletedVideoList(videos);
274      }else{
275        deleted.addVideos(videos);
276      }
277      return this;
278    }
279    
280    /**
281     * 
282     * @param userId
283     * @return this
284     */
285    public FeedbackTaskBuilder setUser(UserIdentity userId){
286      _details.setUserId(userId);
287      return this;
288    }
289    
290    /**
291     * 
292     * @param end
293     * @return this
294     * @throws IllegalArgumentException on null or invalid back-end
295     */
296    public FeedbackTaskBuilder addBackend(AnalysisBackend end) throws IllegalArgumentException{
297      if(end == null || !end.hasCapability(Capability.USER_FEEDBACK)){
298        throw new IllegalArgumentException("The given back-end, id: "+end.getBackendId()+" does not have the required capability: "+Capability.USER_FEEDBACK.name());
299      }
300      _details.setBackend(new BackendStatus(end, TaskStatus.NOT_STARTED));
301      return this;
302    }
303    
304    /**
305     * This will automatically filter out back-end with inadequate capabilities
306     * 
307     * @param backendStatusList
308     * @return this
309     */
310    public FeedbackTaskBuilder setBackends(BackendStatusList backendStatusList){
311      if(BackendStatusList.isEmpty(backendStatusList)){
312        LOGGER.debug("Empty backend status list.");
313        backendStatusList = null;
314      }else if((backendStatusList = BackendStatusList.getBackendStatusList(backendStatusList.getBackendStatuses(EnumSet.of(Capability.USER_FEEDBACK)))) == null){ // filter out back-ends with invalid capabilities
315        LOGGER.warn("List contains no back-ends with valid capability "+Capability.USER_FEEDBACK.name()+"for task type "+TaskType.FEEDBACK.name());
316      }
317      _details.setBackends(backendStatusList);
318      return this;
319    }
320    
321    /**
322     * 
323     * @return this
324     */
325    public FeedbackTaskBuilder clearDeletedVideos(){
326      _details.setDeletedVideoList(null);
327      return this;
328    }
329    
330    /**
331     * 
332     * @return this
333     */
334    public FeedbackTaskBuilder clearVideos(){
335      _details.setVideoList(null);
336      return this;
337    }
338    
339    /**
340     * 
341     * @return this
342     * @throws IllegalArgumentException
343     */
344    private VideoTaskDetails buildFeedback() throws IllegalArgumentException {
345      boolean hasDeleted = !VideoList.isEmpty(_details.getDeletedVideoList());
346      VideoList videoList = _details.getVideoList();
347      boolean hasVideos = !VideoList.isEmpty(videoList);
348      
349      // check for validity:
350      if(hasDeleted){
351        if(hasVideos){
352          throw new IllegalArgumentException("Deleted videos must appear alone.");
353        } // no need to validate the deleted video list, it only requires GUIDs
354      }else if(!hasVideos){
355        LOGGER.debug("No content.");
356        return null;
357      }
358
359      return _details;
360    }
361    
362    /**
363     * 
364     * @return video task details created from the given values
365     * @throws IllegalArgumentException on invalid value combination
366     */
367    private VideoTaskDetails buildBackendFeedback() throws IllegalArgumentException {
368      VideoList videoList = _details.getVideoList();
369      if(VideoList.isEmpty(videoList)){
370        throw new IllegalArgumentException("Back-end feedback must contain video list.");
371      }else if(!DeletedVideoList.isEmpty(_details.getDeletedVideoList())){
372        throw new IllegalArgumentException("Back-end feedback can only contain video list.");
373      }
374
375      return _details;
376    }
377    
378    /**
379     * 
380     * @return new task details based on the given data or null if no data was given
381     */
382    public VideoTaskDetails build() {
383      if(_details.getTaskType() == TaskType.FEEDBACK){
384        return buildFeedback();
385      }else{
386        return buildBackendFeedback();
387      }
388    }
389  } // class FeedbackTaskBuilder
390}