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;
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.Capability;
029import core.tut.pori.context.ServiceInitializer;
030import core.tut.pori.users.UserIdentity;
031
032
033/**
034 * An implementation of ASyncTask, meant for executing a feedback task.
035 * 
036 * Requires a valid taskId for execution, provided in a JobExecutionContext.
037 * 
038 */
039public class PhotoFeedbackTask extends AsyncTask{
040  private static final Logger LOGGER = Logger.getLogger(PhotoFeedbackTask.class);
041  
042  /**
043   * 
044   * @param response
045   * @throws IllegalArgumentException
046   */
047  public static void taskFinished(PhotoTaskResponse response) throws IllegalArgumentException{
048    Integer backendId = response.getBackendId();
049    Long taskId = response.getTaskId();
050
051    PhotoTaskDAO taskDAO = ServiceInitializer.getDAOHandler().getSQLDAO(PhotoTaskDAO.class);
052    BackendStatus taskStatus = taskDAO.getBackendStatus(backendId, taskId);
053    if(taskStatus == null){
054      LOGGER.warn("Backend, id: "+backendId+" returned results for task, not given to the backend. TaskId: "+taskId);
055      throw new IllegalArgumentException("This task is not given for backend, id: "+backendId);
056    }
057
058    TaskStatus status = response.getStatus();
059    if(status == null){
060      LOGGER.warn("Task status not available.");
061      status = TaskStatus.UNKNOWN;
062    }
063    taskStatus.setStatus(status);
064
065    try{
066      PhotoList results = response.getPhotoList();
067      if(PhotoList.isEmpty(results)){
068        LOGGER.warn("No results returned by the backendId: "+backendId);
069        return;
070      }
071
072      if(!PhotoList.isValid(results)){
073        LOGGER.warn("Invalid photoList.");
074      }
075
076      PhotoDAO photoDAO = ServiceInitializer.getDAOHandler().getSolrDAO(PhotoDAO.class);
077      if(!photoDAO.setOwners(results)){
078        LOGGER.warn("Could not get owner information for all photos.");
079      }
080
081      PhotoList associations = new PhotoList();
082      MediaObjectList insert = new MediaObjectList();
083      MediaObjectList update = new MediaObjectList();
084      MediaObjectDAO vdao = ServiceInitializer.getDAOHandler().getSolrDAO(MediaObjectDAO.class);
085      for(Iterator<Photo> photoIter = results.getPhotos().iterator(); photoIter.hasNext();){
086        Photo photo = photoIter.next();
087        String guid = photo.getGUID();
088        UserIdentity userId = photo.getOwnerUserId();
089        if(!UserIdentity.isValid(userId)){  // if this photo does not exist, there won't be userId
090          LOGGER.warn("Ignoring non-existing photo, GUID: "+guid+" from backend, id: "+backendId);
091          continue;
092        }
093        BackendStatusList c = photo.getBackendStatus();
094        if(BackendStatusList.isEmpty(c)){
095          LOGGER.debug("Backend status not available for photo, GUID: "+guid);
096        }else if(c.getCombinedStatus() == TaskStatus.ERROR){
097          LOGGER.warn("Error condition detected for photo, GUID: "+guid);
098        }else{
099          List<BackendStatus> sList = c.getBackendStatuses();
100          if(sList.size() > 1){
101            status = TaskStatus.ERROR;
102            throw new IllegalArgumentException("Multiple backend statuses.");
103          }
104          if(!backendId.equals(sList.get(0).getBackendId())){
105            status = TaskStatus.ERROR;
106            throw new IllegalArgumentException("Invalid backend status.");
107          }
108        }
109        MediaObjectList vObjects = photo.getMediaObjects();
110        if(!MediaObjectList.isEmpty(vObjects)){  // make sure all objects have proper user
111          for(MediaObject mediaObject : vObjects.getMediaObjects()){ // check that the objects are valid
112            if(!backendId.equals(mediaObject.getBackendId())){
113              LOGGER.warn("Task backend id "+backendId+" does not match the backend id "+mediaObject.getBackendId()+" given for media object, objectId: "+mediaObject.getObjectId());
114              mediaObject.setBackendId(backendId);
115            }
116            mediaObject.setOwnerUserId(userId);
117          }
118          vdao.resolveObjectIds(vObjects); // resolve ids for update/insert sort
119          Photo iPhoto = null;
120          for(MediaObject vo : vObjects.getMediaObjects()){ // re-sort to to updated and new
121            if(StringUtils.isBlank(vo.getMediaObjectId())){ // no media object id, this is a new one
122              if(iPhoto == null){
123                associations.getPhoto(guid); // get target photo for insertion
124                if(iPhoto == null){
125                  iPhoto = new Photo(guid);
126                  associations.addPhoto(iPhoto);
127                }
128              }       
129              iPhoto.addMediaObject(vo);
130              insert.addMediaObject(vo);
131            }else{
132              update.addMediaObject(vo);
133            }
134          } // for
135        } // for objects
136      }
137
138      if(MediaObjectList.isEmpty(insert)){
139        LOGGER.debug("Nothing to insert.");
140      }else if(!MediaObjectList.isValid(insert)){
141        status = TaskStatus.ERROR;
142        throw new IllegalArgumentException("Invalid media object list.");
143      }else if(!photoDAO.insert(insert)){
144        LOGGER.warn("Failed to insert new objects.");   
145      }else{
146        photoDAO.associate(associations);
147      }
148
149      if(MediaObjectList.isEmpty(update)){
150        LOGGER.debug("Nothing to update");
151      }else if(!MediaObjectList.isValid(update)){
152        status = TaskStatus.ERROR;
153        throw new IllegalArgumentException("Invalid media object list.");
154      }else if(!photoDAO.update(update)){
155        LOGGER.warn("Failed to update objects.");
156      }
157
158      taskDAO.updateMediaStatus(results.getPhotos(), taskId);
159      taskDAO.updateTaskStatus(taskStatus, taskId);
160    } finally {
161      ServiceInitializer.getEventHandler().publishEvent(new AsyncTaskEvent(backendId, PhotoFeedbackTask.class, status, taskId, TaskType.FEEDBACK));
162    }
163  }
164
165  @Override
166  public void execute(JobExecutionContext context) throws JobExecutionException {
167    executeAddTask(EnumSet.of(Capability.PHOTO_ANALYSIS, Capability.USER_FEEDBACK), ServiceInitializer.getDAOHandler().getSQLDAO(PhotoTaskDAO.class), getTaskId(context.getMergedJobDataMap()));
168  }
169  
170  /**
171   * A helper class building PhotoTaskDetails usable with {@link PhotoFeedbackTask} and executable using {@link service.tut.pori.contentanalysis.CAContentCore#scheduleTask(PhotoTaskDetails)}}
172   * @see service.tut.pori.contentanalysis.CAContentCore
173   * @see service.tut.pori.contentanalysis.PhotoTaskDetails
174   */
175  public static class FeedbackTaskBuilder{
176    private PhotoTaskDetails _details = null;
177    
178    /**
179     * for sub-classing
180     */
181    protected FeedbackTaskBuilder(){
182      // nothing needed
183    }
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 PhotoTaskDetails(taskType);
195    }
196    
197    /**
198     * Add photo to feedback task if the given photo has (valid) changes
199     * 
200     * @param photo
201     * @return this
202     */
203    public FeedbackTaskBuilder addPhoto(Photo photo){
204      if(photo == null){
205        LOGGER.warn("Ignored null photo.");
206      }else{
207        _details.addPhoto(photo);
208      }
209      return this;
210    }
211    
212    /**
213     * 
214     * @param photos
215     * @return this
216     */
217    public FeedbackTaskBuilder addPhotos(PhotoList photos){
218      if(PhotoList.isEmpty(photos)){
219        LOGGER.warn("Ignored empty photo list.");
220      }else{
221        for(Photo p : photos.getPhotos()){
222          addPhoto(p);
223        }
224      }
225      return this;
226    }
227    
228    /**
229     * Set this photo as the reference photo
230     * 
231     * @param photo
232     * @return this
233     * @throws IllegalArgumentException if the given photo is already present in similar or dissimilar photo list
234     */
235    public FeedbackTaskBuilder addReferencePhoto(Photo photo) throws IllegalArgumentException{
236      if(photo == null){
237        LOGGER.warn("Ignored null photo.");
238        return this;
239      }
240      String guid = photo.getGUID();
241      if(StringUtils.isEmpty(guid)){
242        throw new IllegalArgumentException("No GUID for the given photo.");
243      }
244      
245      SimilarPhotoList similar = _details.getSimilarPhotoList();
246      if(!PhotoList.isEmpty(similar) && similar.getPhoto(guid) != null){
247        throw new IllegalArgumentException("Same photo cannot appear in reference list and similar photo list.");
248      }
249      DissimilarPhotoList dissimilar = _details.getDissimilarPhotoList();
250      if(!PhotoList.isEmpty(dissimilar) && dissimilar.getPhoto(guid) != null){
251        throw new IllegalArgumentException("Same photo cannot appear in reference list and dissimilar photo list.");
252      }
253      _details.addReferencePhoto(photo);
254      return this;
255    }
256    
257    /**
258     * 
259     * @param photo
260     * @return this
261     * @throws IllegalArgumentException if the given photo is already present in reference or dissimilar photo list
262     */
263    public FeedbackTaskBuilder addSimilarPhoto(Photo photo) throws IllegalArgumentException{
264      if(photo == null){
265        LOGGER.warn("Ignored null photo.");
266        return this;
267      }
268      
269      String guid = photo.getGUID();
270      if(StringUtils.isEmpty(guid)){
271        throw new IllegalArgumentException("No GUID for the given photo.");
272      }
273      
274      ReferencePhotoList references = _details.getReferencePhotoList();
275      if(!PhotoList.isEmpty(references) && references.getPhoto(guid) != null){
276        throw new IllegalArgumentException("Same photo cannot appear in reference list and similar photo list.");
277      }
278      DissimilarPhotoList dissimilar = _details.getDissimilarPhotoList();
279      if(!PhotoList.isEmpty(dissimilar) && dissimilar.getPhoto(guid) != null){
280        throw new IllegalArgumentException("Same photo cannot appear in similar list and dissimilar photo list.");
281      }
282      _details.addSimilarPhoto(photo);
283      return this;
284    }
285    
286    /**
287     * 
288     * @param photo
289     * @return this
290     * @throws IllegalArgumentException if the given photo is already present in reference or similar photo list
291     */
292    public FeedbackTaskBuilder addDissimilarPhoto(Photo photo) throws IllegalArgumentException{
293      if(photo == null){
294        LOGGER.warn("Ignored invalid photo.");
295        return this;
296      }
297      
298      String guid = photo.getGUID();
299      if(StringUtils.isEmpty(guid)){
300        throw new IllegalArgumentException("No GUID for the given photo.");
301      }
302      
303      ReferencePhotoList references = _details.getReferencePhotoList();
304      if(!PhotoList.isEmpty(references) && references.getPhoto(guid) != null){
305        throw new IllegalArgumentException("Same photo cannot appear in reference list and similar photo list.");
306      }
307      SimilarPhotoList similar = _details.getSimilarPhotoList();
308      if(!PhotoList.isEmpty(similar) && similar.getPhoto(guid) != null){
309        throw new IllegalArgumentException("Same photo cannot appear in similar list and dissimilar photo list.");
310      }
311      _details.addDissimilarPhoto(photo);
312      return this;
313    }
314    
315    /**
316     * 
317     * @param photo
318     * @return this
319     * @throws IllegalArgumentException
320     */
321    public FeedbackTaskBuilder addDeletedPhoto(Photo photo) throws IllegalArgumentException{
322      if(photo == null){
323        LOGGER.warn("Ignored null photo.");
324        return this;
325      }else if(StringUtils.isBlank(photo.getGUID())){
326        throw new IllegalArgumentException("No GUID.");
327      }
328      _details.addDeletedPhoto(photo);
329      return this;
330    }
331    
332    /**
333     * 
334     * @param guids
335     * @return this
336     */
337    public FeedbackTaskBuilder addDeletedPhotos(Collection<String> guids){
338      if(guids == null || guids.isEmpty()){
339        LOGGER.warn("Ignored empty deleted photo list.");
340        return this;
341      }
342      for(String guid : guids){
343        addDeletedPhoto(new Photo(guid));
344      }
345      return this;
346    }
347    
348    /**
349     * 
350     * @param photos
351     * @return this
352     */
353    public FeedbackTaskBuilder addDeletedPhotos(DeletedPhotoList photos){
354      if(DeletedPhotoList.isEmpty(photos)){
355        LOGGER.warn("Ignored empty deleted photo list.");
356        return this;
357      }
358      DeletedPhotoList deleted = _details.getDeletedPhotoList();
359      if(DeletedPhotoList.isEmpty(deleted)){
360        _details.setDeletedPhotoList(photos);
361      }else{
362        deleted.addPhotos(photos);
363      }
364      return this;
365    }
366    
367    /**
368     * 
369     * @param photos
370     * @return this
371     */
372    public FeedbackTaskBuilder addDissimilarPhotos(SimilarPhotoList photos){
373      if(SimilarPhotoList.isEmpty(photos)){
374        LOGGER.warn("Ignored empty similar photo list.");
375        return this;
376      }
377      for(Photo photo : photos.getPhotos()){
378        addSimilarPhoto(photo);
379      }
380      return this;
381    }
382    
383    /**
384     * 
385     * @param photos
386     * @return this
387     */
388    public FeedbackTaskBuilder addSimilarPhotos(DissimilarPhotoList photos){
389      if(DissimilarPhotoList.isEmpty(photos)){
390        LOGGER.warn("Ignored empty dissimilar photo list.");
391        return this;
392      }
393      for(Photo photo : photos.getPhotos()){
394        addDissimilarPhoto(photo);
395      }
396      return this;
397    }
398    
399    /**
400     * 
401     * @param userId
402     * @return this
403     */
404    public FeedbackTaskBuilder setUser(UserIdentity userId){
405      _details.setUserId(userId);
406      return this;
407    }
408    
409    /**
410     * 
411     * @param confidence
412     * @return this
413     */
414    public FeedbackTaskBuilder setUserConfidence(Double confidence){
415      _details.setUserConfidence(confidence);
416      return this;
417    }
418    
419    /**
420     * 
421     * @param end
422     * @return this
423     * @throws IllegalArgumentException on null or invalid back-end
424     */
425    public FeedbackTaskBuilder addBackend(AnalysisBackend end) throws IllegalArgumentException{
426      if(end == null || !end.hasCapability(Capability.USER_FEEDBACK)){
427        throw new IllegalArgumentException("The given back-end, id: "+end.getBackendId()+" does not have the required capability: "+Capability.USER_FEEDBACK.name());
428      }
429      _details.setBackend(new BackendStatus(end, TaskStatus.NOT_STARTED));
430      return this;
431    }
432    
433    /**
434     * This will automatically filter out back-end with inadequate capabilities
435     * 
436     * @param backendStatusList
437     * @return this
438     */
439    public FeedbackTaskBuilder setBackends(BackendStatusList backendStatusList){
440      if(BackendStatusList.isEmpty(backendStatusList)){
441        LOGGER.warn("Empty backend status list.");
442        backendStatusList = null;
443      }else if((backendStatusList = BackendStatusList.getBackendStatusList(backendStatusList.getBackendStatuses(EnumSet.of(Capability.USER_FEEDBACK)))) == null){ // filter out back-ends with invalid capabilities
444        LOGGER.warn("List contains no back-ends with valid capability "+Capability.USER_FEEDBACK.name()+"for task type "+TaskType.FEEDBACK.name());
445      }
446      _details.setBackends(backendStatusList);
447      return this;
448    }
449    
450    /**
451     * 
452     * @return this
453     */
454    public FeedbackTaskBuilder clearDeletedPhotos(){
455      _details.setDeletedPhotoList(null);
456      return this;
457    }
458    
459    /**
460     * 
461     * @return this
462     */
463    public FeedbackTaskBuilder clearSimilarPhotos(){
464      _details.setSimilarPhotoList(null);
465      return this;
466    }
467    
468    /**
469     * 
470     * @return this
471     */
472    public FeedbackTaskBuilder clearDissimilarPhotos(){
473      _details.setDissimilarPhotoList(null);
474      return this;
475    }
476    
477    /**
478     * 
479     * @return this
480     */
481    public FeedbackTaskBuilder clearPhotos(){
482      _details.setPhotoList(null);
483      return this;
484    }
485    
486    /**
487     * 
488     * @return this
489     */
490    public FeedbackTaskBuilder clearReferencePhotos(){
491      _details.setReferencePhotoList(null);
492      return this;
493    }
494    
495    /**
496     * 
497     * @return new task details based on the given data or null if no data was given
498     * @throws IllegalArgumentException on bad data
499     */
500    public PhotoTaskDetails build() throws IllegalArgumentException {
501      boolean hasDeleted = !PhotoList.isEmpty(_details.getDeletedPhotoList());
502      PhotoList photoList = _details.getPhotoList();
503      boolean hasPhotos = !PhotoList.isEmpty(photoList);
504      SimilarPhotoList similarPhotoList = _details.getSimilarPhotoList();
505      boolean hasSimilar = !PhotoList.isEmpty(similarPhotoList);
506      DissimilarPhotoList dissimilarPhotoList = _details.getDissimilarPhotoList();
507      boolean hasDissimilar = !PhotoList.isEmpty(dissimilarPhotoList);
508      ReferencePhotoList referencePhotoList = _details.getReferencePhotoList();
509      boolean hasReferences = !PhotoList.isEmpty(referencePhotoList);
510      
511      // check for validity:
512      if(hasDeleted){
513        if(hasPhotos || hasSimilar || hasDissimilar || hasReferences){
514          throw new IllegalArgumentException("Deleted photos must appear alone.");
515        } // no need to validate the deleted photo list, it only requires guids
516      }else if(hasPhotos){
517        if(hasSimilar || hasDissimilar || hasReferences){
518          throw new IllegalArgumentException("Photos must appear alone.");
519        }else if(!PhotoList.isValid(photoList)){
520          throw new IllegalArgumentException("Invalid photo list.");
521        }
522      }else if(hasReferences){ // this will accept both similar and dissimilar to be present, if they contain valid photos
523        if(!hasDissimilar && !hasSimilar){
524          throw new IllegalArgumentException("References must have similar or dissimilar photos.");
525        }
526        
527        if(hasDissimilar && !DissimilarPhotoList.isValid(dissimilarPhotoList)){
528          throw new IllegalArgumentException("Invalid dissimilar photo list.");
529        }
530        
531        if(hasSimilar && !SimilarPhotoList.isValid(similarPhotoList)){
532          throw new IllegalArgumentException("Invalid similar photo list.");
533        }
534        
535        if(!ReferencePhotoList.isValid(referencePhotoList)){
536          throw new IllegalArgumentException("Invalid reference photo list.");
537        }
538      }else if(hasSimilar || hasDissimilar){
539        throw new IllegalArgumentException("Similar and dissimilar photos cannot appear without references.");
540      }else{
541        LOGGER.debug("No content.");
542        return null;
543      }
544
545      return _details;
546    }
547  } // class FeedbackTaskBuilder
548}