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.Iterator;
022import java.util.List;
023
024import org.apache.log4j.Logger;
025
026import service.tut.pori.contentanalysis.AbstractTaskDetails;
027import service.tut.pori.contentanalysis.AccessDetails;
028import service.tut.pori.contentanalysis.AnalysisBackend.Capability;
029import service.tut.pori.contentanalysis.PhotoParameters;
030import service.tut.pori.contentanalysis.PhotoParameters.AnalysisType;
031import service.tut.pori.contentanalysis.AsyncTask.TaskType;
032import service.tut.pori.contentanalysis.CAContentCore;
033import service.tut.pori.contentanalysis.CAContentCore.ServiceType;
034import service.tut.pori.contentanalysis.CAContentCore.Visibility;
035import service.tut.pori.contentanalysis.DeletedPhotoList;
036import service.tut.pori.contentanalysis.Photo;
037import service.tut.pori.contentanalysis.PhotoDAO;
038import service.tut.pori.contentanalysis.PhotoFeedbackTask.FeedbackTaskBuilder;
039import service.tut.pori.contentanalysis.PhotoList;
040import service.tut.pori.contentanalysis.PhotoTaskDetails;
041import service.tut.pori.contentanalysis.video.DeletedVideoList;
042import service.tut.pori.contentanalysis.video.Video;
043import service.tut.pori.contentanalysis.video.VideoContentCore;
044import service.tut.pori.contentanalysis.video.VideoDAO;
045import service.tut.pori.contentanalysis.video.VideoFeedbackTask;
046import service.tut.pori.contentanalysis.video.VideoList;
047import service.tut.pori.contentanalysis.video.VideoParameters;
048import service.tut.pori.contentanalysis.video.VideoTaskDetails;
049import core.tut.pori.context.ServiceInitializer;
050import core.tut.pori.users.UserIdentity;
051import core.tut.pori.utils.MediaUrlValidator;
052import core.tut.pori.utils.MediaUrlValidator.MediaType;
053import core.tut.pori.utils.UserIdentityLock;
054
055/**
056 * <p>Storage handler that supports saving arbitrary URLs to for the analysis. 
057 * The URLs must be of valid image content. ImageValidator is used for performing a simple content check.</p>
058 * <p>This storage service does not split tasks into smaller chunks like the other content storage services.</p>
059 */
060public final class URLContentStorage extends ContentStorage {
061  /** Service type declaration for this storage */
062  public static final ServiceType SERVICE_TYPE = ServiceType.URL_STORAGE;
063  private static final PhotoParameters ANALYSIS_PARAMETERS_PHOTO;
064  private static final VideoParameters ANALYSIS_PARAMETERS_VIDEO;
065  static{
066    ANALYSIS_PARAMETERS_PHOTO = new PhotoParameters();
067    ANALYSIS_PARAMETERS_PHOTO.setAnalysisTypes(EnumSet.of(AnalysisType.FACE_DETECTION, AnalysisType.KEYWORD_EXTRACTION, AnalysisType.VISUAL));
068    ANALYSIS_PARAMETERS_VIDEO = new VideoParameters();
069    ANALYSIS_PARAMETERS_VIDEO.setSequenceDuration(1);
070    ANALYSIS_PARAMETERS_VIDEO.setAnalysisTypes(EnumSet.of(AnalysisType.VISUAL, AnalysisType.KEYWORD_EXTRACTION));
071  }
072  private static final EnumSet<Capability> CAPABILITIES = EnumSet.of(Capability.PHOTO_ANALYSIS);
073  private static final Logger LOGGER = Logger.getLogger(URLContentStorage.class);
074  private static final UserIdentityLock USER_IDENTITY_LOCK = new UserIdentityLock();
075  
076  /**
077   * 
078   */
079  public URLContentStorage(){
080    super();
081  }
082
083  /**
084   * 
085   * @param autoSchedule
086   */
087  public URLContentStorage(boolean autoSchedule){
088    super(autoSchedule);
089  }
090
091  @Override
092  public ServiceType getServiceType() {
093    return SERVICE_TYPE;
094  }
095
096  @Override
097  public EnumSet<Capability> getBackendCapabilities() {
098    return CAPABILITIES;
099  }
100
101  @Override
102  public String getTargetUrl(AccessDetails details) {
103    return ServiceInitializer.getDAOHandler().getSQLDAO(URLContentDAO.class).getUrl(details.getGuid());
104  }
105
106  @Override
107  public void removeMetadata(UserIdentity userId, Collection<String> guids) {
108    removePhotoMetadata(userId, guids);
109    removeVideoMetadata(userId, guids);
110  }
111  
112  /**
113   * 
114   * @param userId
115   * @param guids
116   */
117  public void removePhotoMetadata(UserIdentity userId, Collection<String> guids) {
118    PhotoDAO photoDAO = ServiceInitializer.getDAOHandler().getSolrDAO(PhotoDAO.class);
119    PhotoList photos = photoDAO.getPhotos(null, guids, null, EnumSet.of(SERVICE_TYPE), new long[]{userId.getUserId()});
120    if(PhotoList.isEmpty(photos)){
121      LOGGER.debug("User, id: "+userId.getUserId()+" has no photos.");
122      return;
123    }
124    List<String> remove = photos.getGUIDs();
125    photoDAO.remove(remove);
126    ServiceInitializer.getDAOHandler().getSQLDAO(URLContentDAO.class).removeEntries(remove);
127
128    FeedbackTaskBuilder builder = new FeedbackTaskBuilder(TaskType.FEEDBACK); // create builder for deleted photo feedback task
129    builder.setUser(userId);
130    builder.addDeletedPhotos(DeletedPhotoList.getPhotoList(photos.getPhotos(), photos.getResultInfo()));
131    builder.setBackends(getBackends());
132    PhotoTaskDetails details = builder.build();
133    if(details == null){
134      LOGGER.warn("No content.");
135    }else{
136      if(isAutoSchedule()){
137        LOGGER.debug("Scheduling feedback task.");
138        CAContentCore.scheduleTask(details);
139      }else{
140        LOGGER.debug("Auto-schedule is disabled.");
141      }
142
143      notifyFeedbackTaskCreated(details);
144    }
145  }
146
147  /**
148   * 
149   * @param userId
150   * @param guids
151   */
152  public void removeVideoMetadata(UserIdentity userId, Collection<String> guids) {
153    VideoDAO videoDAO = ServiceInitializer.getDAOHandler().getSolrDAO(VideoDAO.class);
154    VideoList videos = videoDAO.getVideos(null, guids, null, EnumSet.of(SERVICE_TYPE), new long[]{userId.getUserId()});
155    if(VideoList.isEmpty(videos)){
156      LOGGER.debug("User, id: "+userId.getUserId()+" has no videos.");
157      return;
158    }
159    List<String> remove = videos.getGUIDs();
160    videoDAO.remove(remove);
161    ServiceInitializer.getDAOHandler().getSQLDAO(URLContentDAO.class).removeEntries(remove);
162
163    service.tut.pori.contentanalysis.video.VideoFeedbackTask.FeedbackTaskBuilder builder = new service.tut.pori.contentanalysis.video.VideoFeedbackTask.FeedbackTaskBuilder(TaskType.FEEDBACK); // create builder for deleted video feedback task
164    builder.setUser(userId);
165    builder.addDeletedVideos(DeletedVideoList.getVideoList(videos.getVideos(), videos.getResultInfo()));
166    builder.setBackends(getBackends());
167    VideoTaskDetails details = builder.build();
168    if(details == null){
169      LOGGER.warn("No content.");
170    }else{
171      if(isAutoSchedule()){
172        LOGGER.debug("Scheduling feedback task.");
173        VideoContentCore.scheduleTask(details);
174      }else{
175        LOGGER.debug("Auto-schedule is disabled.");
176      }
177
178      notifyFeedbackTaskCreated(details);
179    }
180  }
181  
182  /**
183   * this will simply remove all non-existing images
184   */
185  @Override
186  public boolean synchronizeAccount(UserIdentity userId) {
187    USER_IDENTITY_LOCK.acquire(userId);
188    LOGGER.debug("Synchronizing account for user, id: "+userId.getUserId());
189    try{
190      URLContentDAO urlContentDAO = ServiceInitializer.getDAOHandler().getSQLDAO(URLContentDAO.class);
191      List<URLEntry> entries = urlContentDAO.getEntries(null, null, null, userId);
192      MediaUrlValidator validator = new MediaUrlValidator();
193      for(Iterator<URLEntry> iter = entries.iterator(); iter.hasNext();){ // loop through the entries, leave invalid uris in the list
194        URLEntry e = iter.next();
195        String url = e.getUrl();
196        MediaType detectedType = validator.validateUrl(url);
197        if(detectedType == MediaType.UNKNOWN){
198          LOGGER.debug("Invalid URL detected: "+url);
199        }else if(!detectedType.equals(e.getMediaType())){
200          LOGGER.warn("Detected type "+detectedType.toInt()+" does not match the stored type "+e.getMediaType().toInt());
201        }else{
202          iter.remove();      
203        }
204      }
205
206      if(!entries.isEmpty()){
207        LOGGER.debug("Removing invalid URLs.");
208        List<String> photoGUIDs = new ArrayList<>();
209        List<String> videoGUIDs = new ArrayList<>();
210        for(URLEntry e : entries){
211          switch(e.getMediaType()){
212            case PHOTO:
213              photoGUIDs.add(e.getGUID());
214              break;
215            case VIDEO:
216              videoGUIDs.remove(e.getGUID());
217              break;
218            default:
219              throw new UnsupportedOperationException("Unhandeled media type: "+e.getMediaType().name());
220          }
221        }
222        
223        if(photoGUIDs.isEmpty()){
224          LOGGER.debug("No photos to remove.");
225        }else{
226          ServiceInitializer.getDAOHandler().getSolrDAO(PhotoDAO.class).remove(photoGUIDs);
227          urlContentDAO.removeEntries(photoGUIDs);
228          CAContentCore.scheduleTask(
229              (new FeedbackTaskBuilder(TaskType.FEEDBACK))
230              .setBackends(getBackends())
231              .addDeletedPhotos(photoGUIDs)
232              .build()
233            );
234        }
235        
236        if(videoGUIDs.isEmpty()){
237          LOGGER.debug("No videos to remove.");
238        }else{
239          ServiceInitializer.getDAOHandler().getSolrDAO(VideoDAO.class).remove(videoGUIDs);
240          urlContentDAO.removeEntries(videoGUIDs);
241          VideoContentCore.scheduleTask(
242              (new VideoFeedbackTask.FeedbackTaskBuilder(TaskType.FEEDBACK))
243              .setBackends(getBackends())
244              .addDeletedVideos(videoGUIDs)
245              .build()
246            );
247        }
248      }
249    }finally{
250      USER_IDENTITY_LOCK.release(userId);
251    }
252    return true;
253  }
254
255  /**
256   * helper method for adding photo URLs
257   * 
258   * @param userId
259   * @param urls list of URLs, validity will NOT be checked
260   * @return the generated task details
261   * @see #addUrls(core.tut.pori.utils.MediaUrlValidator.MediaType, UserIdentity, Collection)
262   */
263  private PhotoTaskDetails addPhotoUrls(UserIdentity userId, Collection<String> urls) {
264    USER_IDENTITY_LOCK.acquire(userId);
265    PhotoList forAnalysis = new PhotoList();
266    try{
267      for(String url : urls){
268        Photo photo = new Photo(null, userId, SERVICE_TYPE, Visibility.PUBLIC);
269        photo.setUrl(url);
270        forAnalysis.addPhoto(photo);
271      }
272
273      PhotoDAO photoDAO = ServiceInitializer.getDAOHandler().getSolrDAO(PhotoDAO.class);
274      URLContentDAO urlContentDAO = ServiceInitializer.getDAOHandler().getSQLDAO(URLContentDAO.class);
275      List<URLEntry> entries = urlContentDAO.getEntries(null, EnumSet.of(MediaType.PHOTO), urls, userId);
276      if(entries == null){ // no previously known URLs
277        photoDAO.insert(forAnalysis); // this will generate GUIDs
278        for(Photo p : forAnalysis.getPhotos()){
279          urlContentDAO.addEntry(new URLEntry(p.getGUID(), MediaType.PHOTO, p.getUrl(), userId));
280        }
281      }else{ // some or all of the URLs are known
282        for(Photo p : forAnalysis.getPhotos()){
283          String url = p.getUrl();
284          String guid = findGUID(entries, url);
285          if(guid == null){ // URL not known previously
286            photoDAO.insert(p); // this will generate GUID
287            urlContentDAO.addEntry(new URLEntry(p.getGUID(), MediaType.PHOTO, url, userId));
288          }else{
289            p.setGUID(guid);
290          }
291        } // for
292      }
293    } finally {
294      USER_IDENTITY_LOCK.release(userId);
295    }
296
297    LOGGER.debug("Creating a new analysis task...");
298    PhotoTaskDetails details = new PhotoTaskDetails(TaskType.ANALYSIS);
299    details.setUserId(userId);
300    details.setBackends(getBackends(getBackendCapabilities()));
301    details.setPhotoList(forAnalysis);
302    details.setTaskParameters(ANALYSIS_PARAMETERS_PHOTO);
303
304    if(isAutoSchedule()){
305      LOGGER.debug("Scheduling photo analysis task.");
306      CAContentCore.scheduleTask(details);
307    }else{
308      LOGGER.debug("Auto-schedule is disabled.");
309    }
310
311    return details;
312  }
313  
314  /**
315   * helper method for finding GUID matching with the given URL from the given list of entries
316   * 
317   * If there are multiple matches, this will return the first match (first depending on the iteration order of the passed collection)
318   * 
319   * @param entries
320   * @param url
321   * @return GUID or null if not found
322   */
323  private String findGUID(Collection<URLEntry> entries, String url){
324    if(entries == null){
325      LOGGER.warn("Null entries.");
326      return null;
327    }
328    
329    for(URLEntry e : entries){
330      if(url.equals(e.getUrl())){
331        return e.getGUID();
332      }
333    }
334    return url;
335  }
336
337  /**
338   * helper method for adding video URLs
339   * 
340   * @param userId
341   * @param urls
342   * @return the generated task details or null if no valid content
343   * @see #addUrls(core.tut.pori.utils.MediaUrlValidator.MediaType, UserIdentity, Collection)
344   */
345  private VideoTaskDetails addVideoUrls(UserIdentity userId, Collection<String> urls) {
346    USER_IDENTITY_LOCK.acquire(userId);
347    VideoList forAnalysis = new VideoList();
348    try{
349      for(String url : urls){
350        Video video = new Video(null, userId, SERVICE_TYPE, Visibility.PUBLIC);
351        video.setUrl(url);
352        forAnalysis.addVideo(video);
353      }
354
355      VideoDAO videoDAO = ServiceInitializer.getDAOHandler().getSolrDAO(VideoDAO.class);
356      URLContentDAO urlContentDAO = ServiceInitializer.getDAOHandler().getSQLDAO(URLContentDAO.class);
357      List<URLEntry> entries = urlContentDAO.getEntries(null, EnumSet.of(MediaType.VIDEO), urls, userId);
358      if(entries == null){ // no previously known urls
359        videoDAO.insert(forAnalysis); // this will generate guids
360        for(Video v : forAnalysis.getVideos()){
361          urlContentDAO.addEntry(new URLEntry(v.getGUID(), MediaType.VIDEO, v.getUrl(), userId));
362        }
363      }else{ // some or all of the urls are known
364        for(Video v : forAnalysis.getVideos()){
365          String url = v.getUrl();
366          String guid = findGUID(entries, url);
367          if(guid == null){ // url not known previously
368            videoDAO.insert(v); // this will generate guid
369            urlContentDAO.addEntry(new URLEntry(v.getGUID(), MediaType.VIDEO, url, userId));
370          }else{
371            v.setGUID(guid);
372          }
373        } // for
374      }
375    } finally {
376      USER_IDENTITY_LOCK.release(userId);
377    }
378
379    LOGGER.debug("Creating a new analysis task...");
380    VideoTaskDetails details = new VideoTaskDetails(TaskType.ANALYSIS);
381    details.setUserId(userId);
382    details.setBackends(getBackends(getBackendCapabilities()));
383    details.setVideoList(forAnalysis);
384    details.setTaskParameters(ANALYSIS_PARAMETERS_VIDEO);
385
386    if(isAutoSchedule()){
387      LOGGER.debug("Scheduling video analysis task.");
388      VideoContentCore.scheduleTask(details);
389    }else{
390      LOGGER.debug("Auto-schedule is disabled.");
391    }
392
393    return details;
394  }
395
396  /**
397   * Add the given list of URLs. 
398   * 
399   * If a given URL already exists in the database, it will not be re-added, but new analysis task for the url will be created and scheduled.
400   * 
401   * Note that this method CANNOT be used to remove previously added (and valid) URLs, which are now invalid. The invalid URLs will simply be ignored.
402   * Use synchronize if you want to clear the database of invalid URLs.
403   * 
404   * @param mediaType 
405   * @param userId
406   * @param urls
407   * @throws IllegalArgumentException on bad input data
408   * @throws UnsupportedOperationException on unsuuported media type
409   * @see #synchronizeAccount(UserIdentity)
410   */
411  public void addUrls(MediaType mediaType, UserIdentity userId, Collection<String> urls) throws IllegalArgumentException, UnsupportedOperationException{
412    if(urls == null || urls.isEmpty()){
413      LOGGER.warn("Empty url list.");
414      return;
415    }
416
417    AbstractTaskDetails details = null;
418    switch(mediaType){
419      case PHOTO:
420        details = addPhotoUrls(userId, urls);
421        break;
422      case VIDEO:
423        details = addVideoUrls(userId, urls);
424        break;
425      default:
426        throw new UnsupportedOperationException("Unsupported media type: "+mediaType.name());
427    }
428
429    if(details == null){
430      throw new IllegalArgumentException("No valid content.");
431    }
432
433    notifyAnalysisTaskCreated(details);
434  }
435  
436  /**
437   * A URL entry.
438   */
439  public static class URLEntry {
440    private String _guid = null;
441    private MediaType _mediaType = null;
442    private String _url = null;
443    private UserIdentity _userId = null;
444    
445    /**
446     * @param guid
447     * @param mediaType
448     * @param url
449     * @param userId
450     */
451    public URLEntry(String guid, MediaType mediaType, String url, UserIdentity userId) {
452      super();
453      _guid = guid;
454      _mediaType = mediaType;
455      _url = url;
456      _userId = userId;
457    }
458    
459    /**
460     * 
461     */
462    protected URLEntry(){
463      // nothing needed
464    }
465
466    /**
467     * @return the guid
468     */
469    public String getGUID() {
470      return _guid;
471    }
472
473    /**
474     * @return the mediaType
475     */
476    public MediaType getMediaType() {
477      return _mediaType;
478    }
479
480    /**
481     * @return the URL
482     */
483    public String getUrl() {
484      return _url;
485    }
486
487    /**
488     * @return the userId
489     */
490    public UserIdentity getUserId() {
491      return _userId;
492    }
493
494    /**
495     * @param guid the GUID to set
496     */
497    protected void setGUID(String guid) {
498      _guid = guid;
499    }
500
501    /**
502     * @param mediaType the mediaType to set
503     */
504    protected void setMediaType(MediaType mediaType) {
505      _mediaType = mediaType;
506    }
507
508    /**
509     * @param url the URL to set
510     */
511    protected void setUrl(String url) {
512      _url = url;
513    }
514
515    /**
516     * @param userId the userId to set
517     */
518    protected void setUserId(UserIdentity userId) {
519      _userId = userId;
520    }
521  } // class URLEntry
522}