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.ArrayList;
019import java.util.Date;
020import java.util.EnumSet;
021import java.util.HashMap;
022import java.util.Iterator;
023import java.util.List;
024import java.util.Map;
025
026import javax.xml.bind.annotation.XmlEnum;
027import javax.xml.bind.annotation.XmlEnumValue;
028
029import org.apache.commons.lang3.ArrayUtils;
030import org.apache.commons.lang3.StringUtils;
031import org.apache.log4j.Logger;
032import org.quartz.JobBuilder;
033import org.quartz.JobDataMap;
034import org.quartz.SchedulerException;
035import org.quartz.Trigger;
036import org.quartz.TriggerBuilder;
037
038import service.tut.pori.contentanalysis.AnalysisBackend.Capability;
039import service.tut.pori.contentanalysis.AsyncTask.TaskStatus;
040import service.tut.pori.contentanalysis.AsyncTask.TaskType;
041import service.tut.pori.contentanalysis.PhotoFeedbackTask.FeedbackTaskBuilder;
042import service.tut.pori.contentstorage.ContentStorageCore;
043import core.tut.pori.context.ServiceInitializer;
044import core.tut.pori.http.RedirectResponse;
045import core.tut.pori.http.parameters.DataGroups;
046import core.tut.pori.http.parameters.Limits;
047import core.tut.pori.users.UserIdentity;
048import core.tut.pori.utils.MediaUrlValidator.MediaType;
049
050/**
051 * 
052 * This class includes functions for general content operations, such as updating the keywords,
053 * modifying photo details, and uploading new content 
054 * 
055 * Note: this does not have UserEventListener for user removal, this is because currently all content is managed by ContentStorage service, which will call all
056 * the necessary method for removing photo content (when needed)
057 */
058public final class CAContentCore {
059  /** default capabilities for photo tasks */
060  public static final EnumSet<Capability> DEFAULT_CAPABILITIES = EnumSet.of(Capability.USER_FEEDBACK, Capability.PHOTO_ANALYSIS, Capability.BACKEND_FEEDBACK);
061  private static final Logger LOGGER = Logger.getLogger(CAContentCore.class);
062
063  /**
064   * Service type declarations.
065   *
066   */
067  @XmlEnum
068  public enum ServiceType {
069    /** content has been retrieved from Picasa, service id: {@value service.tut.pori.contentanalysis.Definitions#SERVICE_ID_PICASA} */
070    @XmlEnumValue(value=Definitions.SERVICE_ID_PICASA)
071    PICASA_STORAGE_SERVICE(1),
072    /** content has been retrieved from FSIO, service id: {@value service.tut.pori.contentanalysis.Definitions#SERVICE_ID_FSIO}  */
073    @Deprecated
074    @XmlEnumValue(value=Definitions.SERVICE_ID_FSIO)
075    FSIO(2),
076    /** 
077     * content has been retrieved from Facebook using the Facebook Jazz Service 
078     * 
079     * service id: {@value service.tut.pori.contentanalysis.Definitions#SERVICE_ID_FACEBOOK_JAZZ} 
080     * 
081     * @see service.tut.pori.facebookjazz.FBJContentCore
082     */
083    @XmlEnumValue(value=Definitions.SERVICE_ID_FACEBOOK_JAZZ)
084    FACEBOOK_JAZZ(3),
085    /** content has been retrieved from Facebook, service id: {@value service.tut.pori.contentanalysis.Definitions#SERVICE_ID_FACEBOOK_PHOTO}  */
086    @XmlEnumValue(value=Definitions.SERVICE_ID_FACEBOOK_PHOTO)
087    FACEBOOK_PHOTO(4),
088    /** 
089     * content has been retrieved from Twitter using the Twitter Jazz Service 
090     * 
091     * service id: {@value service.tut.pori.contentanalysis.Definitions#SERVICE_ID_TWITTER_JAZZ} 
092     * 
093     * @see service.tut.pori.twitterjazz.TJContentCore
094     */
095    @XmlEnumValue(value=Definitions.SERVICE_ID_TWITTER_JAZZ)
096    TWITTER_JAZZ(5),
097    /** content has been retrieved from Twitter, service id: {@value service.tut.pori.contentanalysis.Definitions#SERVICE_ID_TWITTER_PHOTO}  */
098    @XmlEnumValue(value=Definitions.SERVICE_ID_TWITTER_PHOTO)
099    TWITTER_PHOTO(6),
100    /** 
101     * content has been uploaded directly to the service using URLs
102     * 
103     * service id: {@value service.tut.pori.contentanalysis.Definitions#SERVICE_ID_URL_STORAGE} 
104     * 
105     * @see service.tut.pori.contentstorage.ContentStorageCore#addUrls(UserIdentity, int[], List)
106     * */
107    @XmlEnumValue(value=Definitions.SERVICE_ID_URL_STORAGE)
108    URL_STORAGE(7);
109
110    private int _id;
111
112    /**
113     * 
114     * @param id
115     */
116    private ServiceType(int id){
117      _id = id;
118    }
119
120    /**
121     * 
122     * @param id
123     * @return the service id converted to ServiceType
124     * @throws IllegalArgumentException on bad input
125     */
126    public static ServiceType fromServiceId(Integer id) throws IllegalArgumentException{
127      if(id != null){
128        for(ServiceType e : ServiceType.values()){
129          if(e._id == id)
130            return e;
131        }
132      }
133      throw new IllegalArgumentException("Bad "+ServiceType.class.toString()+" : "+id);
134    }
135
136    /**
137     * 
138     * @param types
139     * @return true if the given set was null or empty
140     */
141    public static boolean isEmpty(EnumSet<ServiceType> types){
142      return (types == null || types.isEmpty() ? true : false);
143    }
144
145    /**
146     * 
147     * @return service id of this type
148     */
149    public int getServiceId(){
150      return _id;
151    }
152
153    /**
154     * 
155     * 
156     * @param serviceIds
157     * @return set of service types or null (if empty array is passed OR the array contains only NONE)
158     * @throws IllegalArgumentException on bad input
159     */
160    public static EnumSet<ServiceType> fromIdArray(int[] serviceIds) throws IllegalArgumentException{
161      if(ArrayUtils.isEmpty(serviceIds)){
162        return null;
163      }
164      EnumSet<ServiceType> set = EnumSet.noneOf(ServiceType.class);
165      for(int i=0;i<serviceIds.length;++i){
166        set.add(fromServiceId(serviceIds[i]));
167      }
168      return set;
169    }
170
171    /**
172     * 
173     * @param types
174     * @return the types as integer list or null if null or empty list was passed
175     */
176    public static int[] toIdArray(EnumSet<ServiceType> types){
177      if(ServiceType.isEmpty(types)){
178        LOGGER.debug("No types.");
179        return null;
180      }
181      int[] array = new int[types.size()];
182      int index = 0;
183      for(Iterator<ServiceType> iter = types.iterator(); iter.hasNext(); ++index){
184        array[index] = iter.next().getServiceId();
185      }
186      return array;
187    }
188
189    /**
190     * 
191     * @param set
192     * @return the passed set as a id string (service_id=ID,ID,ID...) or null, if null, empty set, or set containing ServiceType.ALL is passed 
193     */
194    public static String toServiceIdString(EnumSet<ServiceType> set){
195      if(set == null || set.size() < 1){
196        return null;
197      }else{
198        StringBuilder sb = new StringBuilder(Definitions.PARAMETER_SERVICE_ID+"=");
199        Iterator<ServiceType> iter = set.iterator();
200        sb.append(iter.next().getServiceId());
201        while(iter.hasNext()){
202          sb.append(",");
203          sb.append(iter.next().getServiceId());
204        }
205        return sb.toString();
206      }
207    }
208  }  // enum ServiceType
209  
210  /**
211   * The visibility.
212   */
213  @XmlEnum
214  public enum Visibility{
215    /** content can be accessed by anyone */
216    @XmlEnumValue(value=Definitions.VISIBILITY_PUBLIC)
217    PUBLIC(0),
218    /** content can be accessed only by the owner */
219    @XmlEnumValue(value=Definitions.VISIBILITY_PRIVATE)
220    PRIVATE(1),
221    /** content can be accessed only by the users in the defined group */
222    @XmlEnumValue(value=Definitions.VISIBILITY_GROUP)
223    GROUP(2);
224
225    private int _value;
226
227    /**
228     * 
229     * @param value
230     */
231    private Visibility(int value){
232      _value = value;
233    }
234
235    /**
236     * 
237     * @return the visibility as integer
238     */
239    public final int toInt(){
240      return _value;
241    }
242
243    /**
244     * 
245     * @param value
246     * @return the value converted to Visibility
247     * @throws IllegalArgumentException on bad input
248     */
249    public static Visibility fromInt(int value) throws IllegalArgumentException{
250      for(Visibility v : Visibility.values()){
251        if(v._value == value){
252          return v;
253        }
254      }
255      throw new IllegalArgumentException("Bad "+Visibility.class.toString()+" : "+value);
256    }
257  }  // enum Visibility
258
259  /**
260   * 
261   */
262  private CAContentCore() {
263    // nothing needed
264  }
265
266  /**
267   * 
268   * @param response
269   * @throws IllegalArgumentException
270   */
271  public static void taskFinished(PhotoTaskResponse response) throws IllegalArgumentException{
272    validateTaskResponse(response);
273
274    LOGGER.debug("TaskId: "+response.getTaskId()+", backendId: "+response.getBackendId());
275
276    switch(response.getTaskType()){
277      case ANALYSIS:
278        PhotoAnalysisTask.taskFinished(response);
279        break;
280      case BACKEND_FEEDBACK:
281        LOGGER.debug("Using "+PhotoFeedbackTask.class.toString()+" for task of type "+TaskType.BACKEND_FEEDBACK);
282      case FEEDBACK:
283        PhotoFeedbackTask.taskFinished(response);
284        break;
285      case SEARCH:
286        LOGGER.debug("Received taskFinished to a search task: asynchronous search tasks are not supported.");
287      default:
288        throw new IllegalArgumentException("Unsupported "+Definitions.ELEMENT_TASK_TYPE);
289    }
290  }
291
292  /**
293   * 
294   * @param response
295   * @throws IllegalArgumentException on null response, bad task id, bad backend id and/or bad task type
296   */
297  public static void validateTaskResponse(TaskResponse response) throws IllegalArgumentException{
298    if(response == null){
299      throw new IllegalArgumentException("Failed to process response.");
300    }
301    Long taskId = response.getTaskId();
302    if(taskId == null){
303      throw new IllegalArgumentException("Invalid "+Definitions.ELEMENT_TASK_ID);
304    }
305    Integer backendId = response.getBackendId();
306    if(backendId == null){
307      throw new IllegalArgumentException("Invalid "+Definitions.ELEMENT_BACKEND_ID);
308    }
309    TaskType type = response.getTaskType();
310    if(type == null){
311      throw new IllegalArgumentException("Invalid "+Definitions.ELEMENT_TASK_TYPE);
312    }
313  }
314
315  /**
316   * 
317   * @param guid
318   * @param type
319   * @return redirection URL for the given GUID and type or null if either one the given values was null
320   */
321  public static String generateRedirectUrl(String guid, ServiceType type){
322    if(type == null || StringUtils.isBlank(guid)){
323      LOGGER.warn("GUID or service type was null.");
324      return null;
325    }
326    return ServiceInitializer.getPropertyHandler().getRESTBindContext()+Definitions.SERVICE_CA+"/"+Definitions.METHOD_REDIRECT+"?"+Definitions.PARAMETER_GUID+"="+guid+"&"+Definitions.PARAMETER_SERVICE_ID+"="+type.getServiceId();
327  }
328
329  /**
330   * resolves dynamic /rest/r? redirection URL to static access URL
331   * 
332   * @param authenticatedUser
333   * @param serviceType
334   * @param guid
335   * @return redirection to static URL referenced by the given parameters
336   */
337  public static RedirectResponse generateTargetUrl(UserIdentity authenticatedUser, ServiceType serviceType, String guid){
338    return ContentStorageCore.generateTargetUrl(authenticatedUser, serviceType, guid);
339  }
340
341  /**
342   * 
343   * This method is called by back-ends to retrieve a list of photos to be analyzed.
344   * To query tasks status from back-end use queryTaskStatus.
345   * 
346   * @param backendId
347   * @param taskId
348   * @param dataGroups
349   * @param limits
350   * @return the task or null if not found
351   */
352  public static AbstractTaskDetails queryTaskDetails(Integer backendId, Long taskId, DataGroups dataGroups, Limits limits) {
353    return ServiceInitializer.getDAOHandler().getSQLDAO(PhotoTaskDAO.class).getTask(backendId, dataGroups, limits, taskId);
354  }
355
356  /**
357   * Note: if the details already contain a taskId, the task will NOT be re-added to the database, but simply re-scheduled.
358   * 
359   * If the details contains no back-ends, default back-ends will be added. See {@link #DEFAULT_CAPABILITIES}
360   * 
361   * @param details
362   * @return task id of the generated task, null if task could not be created
363   */
364  public static Long scheduleTask(PhotoTaskDetails details) {
365    JobBuilder builder = getBuilder(details.getTaskType());
366    Long taskId = details.getTaskId();
367    if(taskId != null){
368      LOGGER.debug("Task id already present for task, id: "+taskId);
369    }else{
370      BackendStatusList backends = details.getBackends();
371      if(BackendStatusList.isEmpty(backends)){
372        LOGGER.debug("No back-ends given, using defaults...");
373        List<AnalysisBackend> ends = ServiceInitializer.getDAOHandler().getSQLDAO(BackendDAO.class).getBackends(DEFAULT_CAPABILITIES);
374        if(ends == null){
375          LOGGER.warn("Aborting task, no capable back-ends.");
376          return null;
377        }
378        
379        backends = new BackendStatusList();
380        backends.setBackendStatus(ends, TaskStatus.NOT_STARTED);
381        details.setBackends(backends);
382      }
383      
384      taskId = ServiceInitializer.getDAOHandler().getSQLDAO(PhotoTaskDAO.class).insertTask(details);
385      if(taskId == null){
386        LOGGER.error("Task schedule failed: failed to insert new photo task.");
387        return null;
388      }
389    }
390
391    if(scheduleTask(builder, taskId)){
392      return taskId;
393    }else{
394      LOGGER.error("Failed to schedule new task.");
395      return null;
396    }
397  }
398  
399  /**
400   * 
401   * @param builder
402   * @param taskId
403   * @return true if the task was successfully scheduled
404   * @throws IllegalArgumentException on bad values
405   * @see #schedule(JobBuilder)
406   */
407  public static boolean scheduleTask(JobBuilder builder, Long taskId) throws IllegalArgumentException{
408    if(taskId == null || builder == null){
409      throw new IllegalArgumentException("Invalid task id or builder.");
410    }
411    JobDataMap data = new JobDataMap();
412    AsyncTask.setTaskId(data, taskId);
413    builder.setJobData(data);
414    LOGGER.debug("Scheduling task, id: "+taskId);
415    return schedule(builder);
416  }
417  
418  /**
419   * Uses the platform defined scheduler to schedule the given builder. 
420   * This may add a scheduling delay depending on the system property configuration.
421   * 
422   * Note that the task may not necessarily start <i>immediately</i>, but may be delayed because of other tasks already  added into the queue.
423   * 
424   * @param builder
425   * @return true if the job was successfully scheduled
426   * @throws IllegalArgumentException on bad parameters
427   */
428  public static boolean schedule(JobBuilder builder) throws IllegalArgumentException {
429    if(builder == null){
430      throw new IllegalArgumentException("No builder given.");
431    }
432    TriggerBuilder<Trigger> trigger = null;
433    long delay = ServiceInitializer.getPropertyHandler().getSystemProperties(CAProperties.class).getScheduleTaskDelay();
434    if(delay == CAProperties.TASK_DELAY_DISABLED){
435      LOGGER.debug("Scheduling new task to start NOW.");
436      trigger = TriggerBuilder.newTrigger().startNow();
437    }else{
438      LOGGER.debug("Scheduling new task to start in "+delay+" milliseconds.");
439      trigger = TriggerBuilder.newTrigger().startAt(new Date(System.currentTimeMillis()+delay));
440    }
441    
442    try {
443      ServiceInitializer.getExecutorHandler().getScheduler().scheduleJob(builder.build(), trigger.build());
444    } catch (SchedulerException ex) {
445      LOGGER.error(ex, ex);
446      return false;
447    }
448    return true;
449  }
450  
451  /**
452   * 
453   * @param type
454   * @return new builder for the given type
455   * @throws UnsupportedOperationException on unsupported type
456   * @throws IllegalArgumentException on bad type
457   */
458  private static JobBuilder getBuilder(TaskType type) throws UnsupportedOperationException, IllegalArgumentException{
459    if(type == null){
460      throw new IllegalArgumentException("Null type.");
461    }
462    switch (type) {
463      case ANALYSIS:
464        return JobBuilder.newJob(PhotoAnalysisTask.class);
465      case BACKEND_FEEDBACK:
466        return JobBuilder.newJob(PhotoBackendFeedbackTask.class);
467      case FEEDBACK:
468        return JobBuilder.newJob(PhotoFeedbackTask.class);
469      case SEARCH:
470        LOGGER.debug("Task schedule failed: asynchronous search tasks are not supported.");
471      default:
472        throw new UnsupportedOperationException("Unsupported TaskType: "+type.name());
473    }
474  }
475
476  /**
477   * 
478   * @param authenticatedUser
479   * @param guids
480   * @param dataGroups
481   * @param limits
482   * @param serviceTypes
483   * @param userIdFilters
484   * @return list of photos or null if none was found with the given parameters
485   */
486  public static PhotoList getPhotos(UserIdentity authenticatedUser, List<String> guids, DataGroups dataGroups, Limits limits, EnumSet<ServiceType> serviceTypes, long[] userIdFilters){
487    return ServiceInitializer.getDAOHandler().getSolrDAO(PhotoDAO.class).search(authenticatedUser, dataGroups, guids, limits, null, serviceTypes, userIdFilters);
488  }
489
490  /**
491   * This does NOT sync the changes back to content storage (e.g. picasa) to prevent conflicts in future synchronizations
492   * 
493   * @param authenticatedUser
494   * @param photoList
495   * @throws IllegalArgumentException
496   */
497  public static void updatePhotos(UserIdentity authenticatedUser, PhotoList photoList) throws IllegalArgumentException{
498    if(!PhotoList.isValid(photoList)){
499      throw new IllegalArgumentException("Received empty or invalid photoList.");
500    }else{
501      FeedbackTaskBuilder builder = new FeedbackTaskBuilder(TaskType.FEEDBACK);
502      builder.setUser(authenticatedUser);
503      for(Photo photo :  photoList.getPhotos()){
504        if(MediaObjectList.isEmpty(photo.getMediaObjects())){
505          LOGGER.debug("Ignored photo without media objects.");
506        }else{
507          builder.addPhoto(photo);  // add to feedback task
508        }
509      }
510
511      if(!ServiceInitializer.getDAOHandler().getSolrDAO(PhotoDAO.class).updatePhotos(authenticatedUser, photoList)){
512        throw new IllegalArgumentException("Could not update photos.");
513      }
514
515      PhotoTaskDetails details = builder.build();
516      if(details == null){
517        LOGGER.debug("Nothing updated, will not generate feedback.");
518      }else{
519        scheduleTask(details);
520      }
521    }
522  }
523
524  /**
525   * This will allow some amount of bad GUIDs to exist as long as there are enough to make a proper request:
526   * at least one ref photo must exist, at least one similar or dissimilar photo must exist. The GUIDs must be unique,
527   * the same GUID may not appear in similar, dissimilar and ref list.
528   * 
529   * @param authenticatedUser must be given
530   * @param feedbackList
531   */
532  public static void similarityFeedback(UserIdentity authenticatedUser, PhotoFeedbackList feedbackList){
533    if(!UserIdentity.isValid(authenticatedUser)){
534      throw new IllegalArgumentException("Invalid user.");
535    }
536
537    if(!PhotoFeedbackList.isValid(feedbackList)){
538      throw new IllegalArgumentException("Invalid feedback.");
539    }
540    
541    ReferencePhotoList referenceList = feedbackList.getReferencePhotos();
542    List<String> guids = referenceList.getGUIDs();
543    
544    SimilarPhotoList simList = feedbackList.getSimilarPhotos();
545    if(SimilarPhotoList.isEmpty(simList)){
546      LOGGER.debug("No similar photos.");
547      simList = null; // make sure it is really null
548    }else{
549      guids.addAll(simList.getGUIDs());
550    }
551    
552    DissimilarPhotoList disList = feedbackList.getDissimilarPhotos();
553    if(DissimilarPhotoList.isEmpty(disList)){
554      LOGGER.debug("No dissimilar photos.");
555      disList = null; // make sure it is really null
556    }else{
557      guids.addAll(disList.getGUIDs());
558    }
559
560    PhotoList found = ServiceInitializer.getDAOHandler().getSolrDAO(PhotoDAO.class).search(authenticatedUser, null, guids, null, null, null, null); // we use search as the user can give feedback to all photos he/she would get as search results
561    if(PhotoList.isEmpty(found)){
562      LOGGER.warn("Ignored feedback for non-existing or unauthorized photos, for user, id: "+authenticatedUser.getUserId());
563      return;
564    }
565    
566    FeedbackTaskBuilder builder = new FeedbackTaskBuilder(TaskType.FEEDBACK);
567    for(Photo photo : referenceList.getPhotos()){ // validate the given photos through found to make sure no bad data has been given
568      String guid = photo.getGUID();
569      Photo p = found.getPhoto(guid);
570      if(p == null){
571        LOGGER.debug("Ignored reference photo with bad GUID: "+guid+" for user, id: "+authenticatedUser.getUserId());
572      }
573      builder.addReferencePhoto(p);
574    }
575    
576    if(simList != null){
577      for(Photo photo : simList.getPhotos()){ // validate the given photos through found to make sure no bad data has been given
578        String guid = photo.getGUID();
579        Photo p = found.getPhoto(guid);
580        if(p == null){
581          LOGGER.debug("Ignored similar photo with bad GUID: "+guid+" for user, id: "+authenticatedUser.getUserId());
582        }
583        builder.addSimilarPhoto(p);
584      }
585    }
586    
587    if(disList != null){
588      for(Photo photo : disList.getPhotos()){ // validate the given photos through found to make sure no bad data has been given
589        String guid = photo.getGUID();
590        Photo p = found.getPhoto(guid);
591        if(p == null){
592          LOGGER.debug("Ignored dissimilar photo with bad GUID: "+guid+" for user, id: "+authenticatedUser.getUserId());
593        }
594        builder.addDissimilarPhoto(p);
595      }
596    }
597
598    builder.setUser(authenticatedUser);
599    PhotoTaskDetails details = builder.build();
600    if(details == null){
601      throw new IllegalArgumentException("Bad reference list.");
602    }
603    scheduleTask(details);
604  }
605
606  /**
607   * 
608   * @param authenticatedUser
609   * @param dataGroups
610   * @param limits
611   * @param mediaTypes optional media types for the retrieval, if null or empty, all types will be searched for
612   * @param serviceTypes
613   * @param mediaObjectIdFilters
614   * @return list of media objects or null if none was found with the given parameters
615   */
616  public static MediaObjectList getMediaObjects(UserIdentity authenticatedUser, DataGroups dataGroups, Limits limits, EnumSet<MediaType> mediaTypes, EnumSet<ServiceType> serviceTypes, List<String> mediaObjectIdFilters) {
617    MediaObjectList objects = null;
618    if(mediaObjectIdFilters != null){ // if there are ids, convert to objects to use as a filter
619      objects = new MediaObjectList();
620      for(String mediaObjectId : mediaObjectIdFilters){
621        MediaObject o = new MediaObject(); // the media object will have type of UNKNOWN
622        o.setMediaObjectId(mediaObjectId);
623        objects.addMediaObject(o);
624      }
625    }
626    if(mediaTypes == null || mediaTypes.isEmpty()){
627      LOGGER.debug("Empty media type set given, using all...");
628      mediaTypes = EnumSet.allOf(MediaType.class);
629    }
630    return ServiceInitializer.getDAOHandler().getSolrDAO(MediaObjectDAO.class).search(authenticatedUser, dataGroups, limits, mediaTypes, serviceTypes, null, null, objects);
631  }
632
633  /**
634   * Delete the given photos. Normal user (ROLE_USER) can only delete his/her own photos. Back-end user (ROLE_BACKEND) can delete any photos.
635   * 
636   * @param authenticatedUser
637   * @param guids list of guids. Non-existent guids will be ignored.
638   * @return true on success, false on failure. Failure generally means a permission problem.
639   * @throws IllegalArgumentException on bad user id
640   */
641  public static boolean deletePhotos(UserIdentity authenticatedUser, List<String> guids) throws IllegalArgumentException {
642    if(!UserIdentity.isValid(authenticatedUser)){
643      throw new IllegalArgumentException("Invalid user.");
644    }
645    if(guids == null || guids.isEmpty()){
646      LOGGER.warn("Ignored empty guid list.");
647      return true;
648    }
649    
650    PhotoDAO dao = ServiceInitializer.getDAOHandler().getSolrDAO(PhotoDAO.class);
651    List<AccessDetails> details = dao.getAccessDetails(authenticatedUser, guids);
652    if(details == null){
653      LOGGER.debug("None of the guids were found.");
654      return true; // the user has requested that the photos be deleted, and they are gone, so return success
655    }
656    
657    guids = new ArrayList<>(guids.size()); // the found GUIDs
658    for(AccessDetails d : details){ // go through the list of found details and check the permissions
659      switch(d.getPermission()){
660        case BACKEND_ACCESS:
661          LOGGER.debug("Granting delete permissions for user, id: "+authenticatedUser.getUserId()+" for photo, GUID: "+d.getGuid()+" using permissions: "+AccessDetails.Permission.BACKEND_ACCESS.name());
662        case PRIVATE_ACCESS:
663          guids.add(d.getGuid());
664          break;
665        default:
666          LOGGER.warn("Permission denied for user, id: "+authenticatedUser.getUserId()+" for photo, GUID: "+d.getGuid());
667          return false;
668      }
669    }
670    
671    ContentStorageCore.removeMetadata(authenticatedUser, guids, EnumSet.allOf(ServiceType.class)); // remove from content storage (if added)
672    dao.remove(guids); // remove from photo DAO
673    
674    scheduleTask(new FeedbackTaskBuilder(TaskType.FEEDBACK)
675            .setUser(authenticatedUser)
676            .addDeletedPhotos(guids)
677            .build()
678          );
679  
680    return true;
681  }
682  
683  /**
684   * Create and schedule the task for all capable back-ends included in the task designated by the task Id. The given back-end Id will not participate in the feedback task.
685   * 
686   * @param backendId the back-end that send the task finished call, this back-end is automatically omitted from the list of target back-ends
687   * @param photos photos returned in task finished call
688   * @param taskId the id of the finished analysis task
689   */
690  public static void scheduleBackendFeedback(Integer backendId, PhotoList photos, Long taskId){
691    if(PhotoList.isEmpty(photos)){
692      LOGGER.debug("Not scheduling back-end feedback: empty photo list.");
693      return;
694    }
695    
696    BackendStatusList tBackends = ServiceInitializer.getDAOHandler().getSQLDAO(PhotoTaskDAO.class).getBackendStatus(taskId, null);
697    if(BackendStatusList.isEmpty(tBackends)){
698      LOGGER.warn("No back-ends for the given task, or the task does not exist. Task id: "+taskId);
699      return;
700    }
701    
702    List<AnalysisBackend> backends = ServiceInitializer.getDAOHandler().getSQLDAO(BackendDAO.class).getBackends(Capability.BACKEND_FEEDBACK); // get list of back-ends with compatible capabilities
703    if(backends == null){
704      LOGGER.debug("No capable back-ends for back-end feedback.");
705      return;
706    }
707
708    BackendStatusList statuses = new BackendStatusList();
709    for(AnalysisBackend backend : backends){
710      Integer id = backend.getBackendId();
711      if(id.equals(backendId)){ // ignore the given back-end id
712        LOGGER.debug("Ignoring the back-end id of task results, back-end id: "+backendId+", task, id: "+taskId);
713      }else if(tBackends.getBackendStatus(id) != null){ // and all back-ends not part of the task
714        statuses.setBackendStatus(new BackendStatus(backend, TaskStatus.NOT_STARTED));
715      }
716    }
717    if(BackendStatusList.isEmpty(statuses)){
718      LOGGER.debug("No capable back-ends for back-end feedback.");
719      return;
720    }
721    
722    PhotoTaskDetails details = (new service.tut.pori.contentanalysis.PhotoBackendFeedbackTask.FeedbackTaskBuilder(TaskType.BACKEND_FEEDBACK))
723        .setBackends(statuses)
724        .addPhotos(photos)
725        .build();
726    Map<String, String> metadata = new HashMap<>(1);
727    metadata.put(Definitions.METADATA_RELATED_TASK_ID, taskId.toString());
728    details.setMetadata(metadata);
729    
730    scheduleTask(details);
731  }
732}