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.contentstorage;
017
018import java.util.ArrayList;
019import java.util.Collection;
020import java.util.EnumSet;
021import java.util.HashMap;
022import java.util.List;
023import java.util.Map;
024
025import org.apache.commons.lang3.ArrayUtils;
026import org.apache.commons.lang3.StringUtils;
027import org.apache.log4j.Logger;
028import org.quartz.Job;
029import org.quartz.JobBuilder;
030import org.quartz.JobDataMap;
031import org.quartz.JobExecutionContext;
032import org.quartz.JobExecutionException;
033import org.springframework.context.ApplicationListener;
034
035import service.tut.pori.contentanalysis.AbstractTaskDetails;
036import service.tut.pori.contentanalysis.AccessDetails;
037import service.tut.pori.contentanalysis.AccessDetails.Permission;
038import service.tut.pori.contentanalysis.AnalysisBackend;
039import service.tut.pori.contentanalysis.AsyncTask;
040import service.tut.pori.contentanalysis.AsyncTask.TaskStatus;
041import service.tut.pori.contentanalysis.AsyncTask.TaskType;
042import service.tut.pori.contentanalysis.BackendDAO;
043import service.tut.pori.contentanalysis.BackendStatus;
044import service.tut.pori.contentanalysis.BackendStatusList;
045import service.tut.pori.contentanalysis.CAContentCore;
046import service.tut.pori.contentanalysis.CAContentCore.ServiceType;
047import service.tut.pori.contentanalysis.Media;
048import service.tut.pori.contentanalysis.Photo;
049import service.tut.pori.contentanalysis.PhotoDAO;
050import service.tut.pori.contentanalysis.PhotoList;
051import service.tut.pori.contentanalysis.PhotoTaskDetails;
052import service.tut.pori.contentanalysis.TaskDAO;
053import service.tut.pori.contentanalysis.video.Video;
054import service.tut.pori.contentanalysis.video.VideoList;
055import service.tut.pori.contentanalysis.video.VideoTaskDetails;
056import service.tut.pori.users.facebook.FacebookUserCore;
057import service.tut.pori.users.google.GoogleUserCore;
058import service.tut.pori.users.twitter.TwitterUserCore;
059import core.tut.pori.context.ServiceInitializer;
060import core.tut.pori.http.RedirectResponse;
061import core.tut.pori.users.UserEvent;
062import core.tut.pori.users.UserEvent.EventType;
063import core.tut.pori.users.UserIdentity;
064import core.tut.pori.utils.ListUtils;
065import core.tut.pori.utils.MediaUrlValidator;
066import core.tut.pori.utils.MediaUrlValidator.MediaType;
067
068/**
069 * The core methods for managing metadata content storage.
070 */
071public final class ContentStorageCore {
072  private static final String JOB_KEY_SERVICE_TYPES = "serviceType"; // type is EnumSet<ServiceType>
073  private static final String JOB_KEY_USER_ID = "userId"; // type is Long
074  private static final Logger LOGGER = Logger.getLogger(ContentStorageCore.class);
075  
076  /**
077   * 
078   */
079  private ContentStorageCore(){
080    // nothing needed
081  }
082  
083  /**
084   * 
085   * @param backendIds
086   * @param status
087   * @return all of the requested back-ends with the given status
088   * @throws IllegalArgumentException on invalid id or if no back-ends are available
089   */
090  private static BackendStatusList getBackendStatuses(int[] backendIds, TaskStatus status) throws IllegalArgumentException {
091    BackendStatusList backendStatusList = new BackendStatusList();
092    BackendDAO backendDAO = ServiceInitializer.getDAOHandler().getSQLDAO(BackendDAO.class);
093    if(!ArrayUtils.isEmpty(backendIds)){
094      LOGGER.debug("Adding back-ends...");
095      List<Integer> backendIdList = ListUtils.createList(backendIds);
096      List<AnalysisBackend> backends = backendDAO.getBackends(backendIdList);
097      if(backends == null){
098        throw new IllegalArgumentException("Invalid back-end ids.");
099      }
100      for(Integer backendId : backendIdList){ // validate the user given back-end ids
101        AnalysisBackend end = null;
102        for(AnalysisBackend backend : backends){
103          if(backend.getBackendId().equals(backendId)){
104            end = backend;
105            break;
106          }
107        }
108        if(end == null){
109          throw new IllegalArgumentException("Invalid back-end id: "+backendId);
110        }
111        backendStatusList.setBackendStatus(new BackendStatus(end, status));
112      }
113    }else{
114      LOGGER.debug("No back-ends given, attempting to use defaults...");
115      List<AnalysisBackend> backends = backendDAO.getEnabledBackends();
116      if(backends == null){
117        throw new IllegalArgumentException("No enabled back-ends available.");
118      }
119      for(AnalysisBackend end : backends){
120        backendStatusList.setBackendStatus(new BackendStatus(end, status));
121      }
122    }
123    return backendStatusList;
124  }
125  
126  /**
127   * 
128   * @param authenticatedUser
129   * @param backendIds
130   * @param serviceTypes
131   * @return task id of the scheduled task or null if schedule failed
132   */
133  public static Long synchronize(UserIdentity authenticatedUser, int[] backendIds, EnumSet<ServiceType> serviceTypes) {
134    SynchronizationTaskDetails details = new SynchronizationTaskDetails();
135    details.setBackends(getBackendStatuses(backendIds, TaskStatus.NOT_STARTED));
136    SynchronizationTaskDetails.setServiceTypes(details, serviceTypes);
137    details.setUserId(authenticatedUser);
138    
139    Long taskId = ServiceInitializer.getDAOHandler().getSQLDAO(TaskDAO.class).insertTask(details); // use the default implementation to insert as there is only metadata content
140    if(taskId == null){
141      LOGGER.warn("Failed to add new synchronization task.");
142    }else{
143      CAContentCore.scheduleTask(JobBuilder.newJob(MetadataSynchronizationJob.class), taskId);
144    }
145    return taskId;
146  }
147  
148  /**
149   * resolves dynamic /rest/r? redirection URL to static access URL
150   * 
151   * @param authenticatedUser
152   * @param serviceType
153   * @param guid
154   * @return redirection to dynamic URL
155   */
156  public static RedirectResponse generateTargetUrl(UserIdentity authenticatedUser, ServiceType serviceType, String guid){
157    AccessDetails details = ServiceInitializer.getDAOHandler().getSolrDAO(PhotoDAO.class).getAccessDetails(authenticatedUser, guid);
158    if(details == null){
159      throw new IllegalArgumentException("Not Found.");
160    }
161    Permission access = details.getPermission();
162    if(access == Permission.NO_ACCESS){
163      LOGGER.debug("Access denied for GUID: "+guid+" for userId: "+(UserIdentity.isValid(authenticatedUser) ? authenticatedUser.getUserId() : "none"));
164      throw new IllegalArgumentException("Not Found.");
165    }
166    LOGGER.debug("Granting access with "+Permission.class.toString()+" : "+access.name());
167    
168    String url = getContentStorage(false, serviceType).getTargetUrl(details);
169    if(url == null){
170      throw new IllegalArgumentException("Not Found.");
171    }else{
172      return new RedirectResponse(url);
173    }
174  }
175  
176  /**
177   * 
178   * @param autoScheduleEnabled
179   * @param serviceType
180   * @return content storage for the given service type
181   * @throws UnsupportedOperationException
182   */
183  public static final ContentStorage getContentStorage(boolean autoScheduleEnabled, ServiceType serviceType) throws UnsupportedOperationException{
184    switch(serviceType){
185      case PICASA_STORAGE_SERVICE:
186        return new PicasaCloudStorage(autoScheduleEnabled);
187      case FACEBOOK_PHOTO:
188        return new FacebookPhotoStorage(autoScheduleEnabled);
189      case TWITTER_PHOTO:
190        return new TwitterPhotoStorage(autoScheduleEnabled);
191      case URL_STORAGE:
192        return new URLContentStorage(autoScheduleEnabled);
193      default:
194        throw new UnsupportedOperationException("Unsupported ServiceType: "+serviceType.name());
195    }
196  }
197  
198  /**
199   * Removes all (photo) metadata associated with the given user. 
200   * This contains all service specific entries and the related media and media objects.
201   * 
202   * @param authenticatedUser
203   * @param guids optional filter
204   * @param serviceTypes
205   */
206  public static void removeMetadata(UserIdentity authenticatedUser, Collection<String> guids, EnumSet<ServiceType> serviceTypes){
207    if(ServiceType.isEmpty(serviceTypes)){
208      LOGGER.warn("No service types given.");
209      return;
210    }
211    
212    for(ServiceType type : serviceTypes){
213      try{
214        getContentStorage(true, type).removeMetadata(authenticatedUser, guids);
215      }catch(UnsupportedOperationException ex){
216        LOGGER.warn(ex, ex);
217      }
218    } // for
219  }
220  
221  /**
222   * 
223   * @param authenticatedUser
224   * @param backendIds
225   * @param urls
226   * @return details of the files added to the analysis task, note that not all files are necessary new ones, if the given URLs were already known by the system
227   */
228  public static MediaList addUrls(UserIdentity authenticatedUser, int[] backendIds, List<String> urls) {
229    List<String> photoUrls = new ArrayList<>();
230    List<String> videoUrls = new ArrayList<>();
231    MediaUrlValidator validator = new MediaUrlValidator();
232    for(String url : urls){
233      switch(validator.validateUrl(url)){
234        case PHOTO:
235          LOGGER.debug("Detected photo: "+url);
236          photoUrls.add(url);
237          break;
238        case VIDEO:
239          LOGGER.debug("Detected video: "+url);
240          videoUrls.add(url);
241          break;
242        default:
243          LOGGER.warn("Unknown media type for URL: "+url);
244          break;
245      }
246    }
247    
248    BackendStatusList backends = getBackendStatuses(backendIds, TaskStatus.NOT_STARTED);
249    URLContentStorage storage = new URLContentStorage();
250    storage.setBackends(backends);
251    ContentStorageListener listener = new ContentStorageListener();
252    storage.setContentStorageListener(listener);
253    
254    if(photoUrls.isEmpty()){
255      LOGGER.debug("No photo URLs.");
256    }else{
257      storage.addUrls(MediaType.PHOTO, authenticatedUser, photoUrls);
258    }
259    
260    if(videoUrls.isEmpty()){
261      LOGGER.debug("No video URLs.");
262    }else{
263      storage.addUrls(MediaType.VIDEO, authenticatedUser, videoUrls);
264    }
265    
266    return listener.getMediaList();
267  }
268  
269  /**
270   * Listener for user related events.
271   *
272   * Automatically instantiated by Spring as a bean.
273   */
274  @SuppressWarnings("unused")
275  private static class UserEventListener implements ApplicationListener<UserEvent>{
276
277    @Override
278    public void onApplicationEvent(UserEvent event) {
279      EventType type = event.getType();
280      switch(type){
281        case USER_AUTHORIZATION_REVOKED:
282          Long userId = event.getUserId().getUserId();
283          LOGGER.debug("Detected event of type "+type.name()+", scheduling removal of all metadata content for user, id: "+userId);
284          Class<?> source = event.getSource();
285          if(source == FacebookUserCore.class){
286            createJob(userId, EnumSet.of(ServiceType.FACEBOOK_PHOTO));
287          }else if(source == GoogleUserCore.class){
288            createJob(userId, EnumSet.of(ServiceType.PICASA_STORAGE_SERVICE));
289          }else if(source == TwitterUserCore.class){
290            createJob(userId, EnumSet.of(ServiceType.TWITTER_PHOTO));
291          }
292          break;
293        case USER_REMOVED:
294          userId = event.getUserId().getUserId();
295          LOGGER.debug("Detected event of type "+type.name()+", scheduling removal of all metadata content for user, id: "+userId);
296          createJob(userId, EnumSet.allOf(ServiceType.class));
297          break;
298        default: // ignore everything else
299          break;
300      }
301    }
302    
303    /**
304     * 
305     * @param userId
306     * @param serviceTypes
307     */
308    private void createJob(Long userId, EnumSet<ServiceType> serviceTypes){
309      JobBuilder builder = JobBuilder.newJob(MetadataRemovalJob.class);
310      JobDataMap data = new JobDataMap();
311      data.put(JOB_KEY_USER_ID, userId);
312      data.put(JOB_KEY_SERVICE_TYPES, serviceTypes);
313      builder.setJobData(data);
314      CAContentCore.schedule(builder);
315    }
316  } // class UserEventListener
317  
318  /**
319   * 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
320   *
321   */
322  public static class MetadataRemovalJob implements Job{
323
324    @SuppressWarnings("unchecked")
325    @Override
326    public void execute(JobExecutionContext context) throws JobExecutionException {
327      JobDataMap data = context.getMergedJobDataMap();
328      Long userId = data.getLong(JOB_KEY_USER_ID);
329      LOGGER.debug("Removing all metadata content for user, id: "+userId);
330      removeMetadata(new UserIdentity(userId), null, (EnumSet<ServiceType>) data.get(JOB_KEY_SERVICE_TYPES));
331    }
332  } // class MetadataRemovalJob
333  
334  /**
335   * Implementation of AbstractTaskDetails used internally for scheduling synchronization tasks.
336   */
337  public static class SynchronizationTaskDetails extends AbstractTaskDetails{
338    private static final String METADATA_SERVICE_TYPES = "serviceTypes";
339    
340    /**
341     * 
342     */
343    public SynchronizationTaskDetails(){
344      super();
345      setTaskType(TaskType.UNDEFINED);
346    }
347    
348    /**
349     * 
350     * @param details
351     * @param serviceTypes
352     */
353    public static void setServiceTypes(AbstractTaskDetails details, EnumSet<ServiceType> serviceTypes){
354      Map<String, String> metadata = details.getMetadata();
355      if(serviceTypes == null || serviceTypes.isEmpty()){
356        LOGGER.debug("No service types.");
357        if(metadata != null){
358          metadata.remove(METADATA_SERVICE_TYPES);
359          if(metadata.isEmpty()){
360            details.setMetadata(null);
361          }
362        }
363        return;
364      }
365      StringBuilder cb = new StringBuilder();
366      for(ServiceType s : serviceTypes){
367        cb.append(s.getServiceId());
368        cb.append(core.tut.pori.http.Definitions.SEPARATOR_URI_QUERY_PARAM_VALUES);
369      }
370      if(metadata == null){
371        metadata = new HashMap<>(1);
372      }
373      metadata.put(METADATA_SERVICE_TYPES, cb.substring(0, cb.length()-1));
374      details.setMetadata(metadata);
375    }
376    
377    /**
378     * 
379     * @param details 
380     * @return service types associated with the details
381     */
382    public static EnumSet<ServiceType> getServiceTypes(AbstractTaskDetails details){
383      Map<String, String> metadata = details.getMetadata();
384      if(metadata == null || metadata.isEmpty()){
385        LOGGER.debug("No metadata.");
386        return null;
387      }
388
389      String[] serviceTypes = StringUtils.split(metadata.get(METADATA_SERVICE_TYPES), core.tut.pori.http.Definitions.SEPARATOR_URI_QUERY_PARAM_VALUES);
390      if(serviceTypes == null){
391        LOGGER.debug("No service names.");
392        return null;
393      }
394      
395      EnumSet<ServiceType> types = EnumSet.noneOf(ServiceType.class);
396      for(int i=0;i<serviceTypes.length;++i){
397        types.add(ServiceType.fromServiceId(Integer.valueOf(serviceTypes[i])));
398      }
399      return types;
400    }
401
402    @Override
403    public TaskParameters getTaskParameters() {
404      return null;
405    }
406
407    @Override
408    public void setTaskParameters(TaskParameters parameters) throws UnsupportedOperationException {
409      throw new UnsupportedOperationException("Method not supported.");     
410    }
411  } // class SynchronizationTaskDetails
412  
413  /**
414   * A schedulable task used for synchronizing metadata
415   */
416  public static class MetadataSynchronizationJob implements Job{
417
418    @Override
419    public void execute(JobExecutionContext context) throws JobExecutionException {
420      JobDataMap data = context.getMergedJobDataMap();
421      Long taskId = AsyncTask.getTaskId(data);
422      if(taskId == null){
423        LOGGER.warn("No taskId.");
424        return;
425      }
426
427      TaskDAO taskDAO = ServiceInitializer.getDAOHandler().getSQLDAO(TaskDAO.class);
428      BackendStatusList backends = taskDAO.getBackendStatus(taskId, TaskStatus.NOT_STARTED);
429      if(BackendStatusList.isEmpty(backends)){
430        LOGGER.warn("No analysis back-ends available for taskId: "+taskId+" with status "+TaskStatus.NOT_STARTED.name());
431        return;
432      }
433      
434      AbstractTaskDetails details = taskDAO.getTask(null, null, null, taskId); // no need to retrieve per back-end as the details are the same for each back-end
435      if(details == null){
436        LOGGER.warn("Task not found, id: "+taskId);
437        return;
438      }
439
440      UserIdentity userId = details.getUserId();
441      LOGGER.debug("Execution started for user id: "+userId.getUserId());
442      
443      for(ServiceType type : SynchronizationTaskDetails.getServiceTypes(details)){
444        try{
445          ContentStorage storage = getContentStorage(true, type);
446          storage.setBackends(backends);
447          if(!storage.synchronizeAccount(userId)){
448            LOGGER.warn("Failed to synchronize service of type "+type.name()+" for user, id: "+userId.getUserId());
449          }
450        }catch(Throwable ex){ // catch exceptions to prevent re-scheduling of the task on error
451          LOGGER.warn(ex, ex);
452        }
453      }
454      LOGGER.debug("Synchronization completed.");
455    } 
456  } // class MetadataSynchronizationJob
457  
458  /**
459   * internally used listener class
460   *
461   */
462  private static class ContentStorageListener implements service.tut.pori.contentstorage.ContentStorage.ContentStorageListener {
463    private PhotoList _analysisTaskPhotoList = null;
464    private VideoList _analysisTaskVideoList = null;
465    
466    @Override
467    public void analysisTaskCreated(AbstractTaskDetails details) {
468      if(details != null){
469        if(details instanceof PhotoTaskDetails){
470          _analysisTaskPhotoList = ((PhotoTaskDetails) details).getPhotoList();
471        }else if(details instanceof VideoTaskDetails){
472          _analysisTaskVideoList = ((VideoTaskDetails) details).getVideoList();
473        }else{
474          LOGGER.debug("Ignored unsupported task details of type "+details.getClass());
475        }
476      }else{
477        LOGGER.warn("Received null task details.");
478      }
479    }
480
481    @Override
482    public void feedbackTaskCreated(AbstractTaskDetails details) {
483      // nothing needed
484    }
485    
486    /**
487     * 
488     * @return combined media list of task photos and videos
489     */
490    public MediaList getMediaList() {
491      if(_analysisTaskPhotoList == null && _analysisTaskVideoList == null){
492        return null;
493      }
494      
495      List<Media> media = new ArrayList<>(PhotoList.count(_analysisTaskPhotoList)+VideoList.count(_analysisTaskVideoList));
496      if(_analysisTaskPhotoList != null){
497        for(Photo p : _analysisTaskPhotoList.getPhotos()){
498          media.add(p);
499        }
500      }
501      if(_analysisTaskVideoList != null){
502        for(Video v : _analysisTaskVideoList.getVideos()){
503          media.add(v);
504        }
505      }
506      
507      MediaList mediaList = new MediaList();
508      mediaList.setMedia(media);
509      return mediaList;
510    }
511  } // class ContentStorageListener
512}