001/**
002 * Copyright 2015 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.Arrays;
019import java.util.Collection;
020import java.util.Date;
021import java.util.EnumSet;
022import java.util.Iterator;
023import java.util.List;
024import java.util.Map;
025import java.util.Map.Entry;
026import java.util.Set;
027import java.util.UUID;
028
029import org.apache.commons.lang3.ArrayUtils;
030import org.apache.log4j.Logger;
031import org.apache.solr.client.solrj.response.QueryResponse;
032import org.apache.solr.client.solrj.response.UpdateResponse;
033import org.apache.solr.common.SolrException;
034import org.springframework.beans.factory.annotation.Autowired;
035
036import service.tut.pori.contentanalysis.AccessDetails;
037import service.tut.pori.contentanalysis.AssociationDAO;
038import service.tut.pori.contentanalysis.CAContentCore.ServiceType;
039import service.tut.pori.contentanalysis.CAContentCore.Visibility;
040import service.tut.pori.contentanalysis.ResultInfo;
041import service.tut.pori.contentanalysis.MediaObject;
042import service.tut.pori.contentanalysis.MediaObjectDAO;
043import service.tut.pori.contentanalysis.MediaObjectList;
044import core.tut.pori.dao.SQLSelectBuilder.OrderDirection;
045import core.tut.pori.dao.SimpleSolrTemplate;
046import core.tut.pori.dao.SolrDAO;
047import core.tut.pori.dao.SolrQueryBuilder;
048import core.tut.pori.dao.filter.AbstractQueryFilter;
049import core.tut.pori.dao.filter.AndQueryFilter;
050import core.tut.pori.dao.filter.AndSubQueryFilter;
051import core.tut.pori.dao.filter.OrQueryFilter;
052import core.tut.pori.http.parameters.DataGroups;
053import core.tut.pori.http.parameters.Limits;
054import core.tut.pori.http.parameters.SortOptions;
055import core.tut.pori.users.UserIdentity;
056import core.tut.pori.utils.MediaUrlValidator.MediaType;
057
058/**
059 * The DAO for storing and retrieving video objects.
060 */
061public class VideoDAO extends SolrDAO {
062  private static final String BEAN_ID_SOLR_SERVER = "solrServerVideos";
063  private static final SortOptions DEFAULT_SORT_OPTIONS;
064  static{
065    DEFAULT_SORT_OPTIONS = new SortOptions();
066    DEFAULT_SORT_OPTIONS.addSortOption(new SortOptions.Option(SOLR_FIELD_ID, OrderDirection.ASCENDING, Definitions.ELEMENT_VIDEOLIST));
067  }
068  private static final String[] FIELDS_DATA_GROUP_DEFAULTS = new String[]{SOLR_FIELD_ID, Definitions.SOLR_FIELD_SERVICE_ID, Definitions.SOLR_FIELD_USER_ID};
069  private static final String FIELDS_DATA_GROUP_VISIBILITY = Definitions.SOLR_FIELD_VISIBILITY;
070  private static final String[] FIELDS_DATA_GROUP_BASIC = ArrayUtils.addAll(FIELDS_DATA_GROUP_DEFAULTS, FIELDS_DATA_GROUP_VISIBILITY, Definitions.SOLR_FIELD_CREDITS, Definitions.SOLR_FIELD_NAME, Definitions.SOLR_FIELD_DESCRIPTION);
071  private static final String[] FIELDS_GET_ACCESS_DETAILS = new String[]{Definitions.SOLR_FIELD_USER_ID, Definitions.SOLR_FIELD_VISIBILITY, SOLR_FIELD_ID};
072  private static final String[] FIELDS_SET_OWNERS = new String[]{SOLR_FIELD_ID, Definitions.SOLR_FIELD_USER_ID};
073  private static final Logger LOGGER = Logger.getLogger(VideoDAO.class);
074  private static final EnumSet<MediaType> MEDIA_TYPES = EnumSet.of(MediaType.VIDEO);
075  @Autowired
076  private AssociationDAO _associationDAO = null;
077  @Autowired
078  private VideoTaskDAO _videoTaskDAO = null;
079  @Autowired
080  private MediaObjectDAO _mediaObjectDAO = null;
081  
082  /**
083   * Inserts the objects and sets all media types to {@link core.tut.pori.utils.MediaUrlValidator.MediaType#VIDEO} for objects with {@link core.tut.pori.utils.MediaUrlValidator.MediaType#UNKNOWN} or null media type.
084   * @param objects
085   * @return true on success
086   * @see service.tut.pori.contentanalysis.MediaObjectDAO#insert(MediaObjectList)
087   */
088  public boolean insert(MediaObjectList objects){
089    if(MediaObjectList.isEmpty(objects)){
090      LOGGER.debug("Empty list ignored.");
091      return true;
092    }
093    for(MediaObject object : objects.getMediaObjects()){
094      MediaType mediaType = object.getMediaType();
095      if(mediaType == null || MediaType.UNKNOWN.equals(mediaType)){
096        object.setMediaType(MediaType.VIDEO); //set all media object MediaType to VIDEO
097      }
098    }
099    return _mediaObjectDAO.insert(objects);
100  }
101  
102  /**
103   * Update the objects and sets all media types to {@link core.tut.pori.utils.MediaUrlValidator.MediaType#VIDEO} for objects with {@link core.tut.pori.utils.MediaUrlValidator.MediaType#UNKNOWN} or null media type.
104   * @param objects
105   * @return true on success
106   * @see service.tut.pori.contentanalysis.MediaObjectDAO#update(MediaObjectList)
107   */
108  public boolean update(MediaObjectList objects){
109    if(MediaObjectList.isEmpty(objects)){
110      LOGGER.debug("Empty list ignored.");
111      return true;
112    }
113    for(MediaObject object : objects.getMediaObjects()){
114      MediaType mediaType = object.getMediaType();
115      if(mediaType == null || MediaType.UNKNOWN.equals(mediaType)){
116        object.setMediaType(MediaType.VIDEO); //set all media object MediaType to VIDEO
117      }
118    }
119    return _mediaObjectDAO.update(objects);
120  }
121  
122  /**
123   * 
124   * @param video
125   * @return true on success
126   */
127  public boolean insert(Video video){
128    VideoList vl = new VideoList();
129    vl.setVideos(Arrays.asList(video));
130    return insert(vl);
131  }
132
133  /**
134   * 
135   * @param videos
136   * @return true on success
137   */
138  public boolean insert(VideoList videos){
139    if(VideoList.isEmpty(videos)){
140      LOGGER.debug("No videos given.");
141      return false;
142    }
143    LOGGER.debug("Adding videos...");
144    List<Video> v = videos.getVideos();
145    Date updated = new Date();
146    MediaObjectList combined = new MediaObjectList();
147    for(Video video : v){
148      if(video.getUpdated() == null){
149        video.setUpdated(updated);
150      }
151      
152      String guid = UUID.randomUUID().toString();
153      if(video.getGUID() != null){
154        LOGGER.warn("Replacing GUID for video with existing GUID: "+video.getGUID()+", new GUID: "+guid);   
155      }
156      video.setGUID(guid);
157
158      MediaObjectList objects = video.getMediaObjects();
159      Visibility visibility = video.getVisibility();
160      UserIdentity userId = video.getOwnerUserId();
161      if(!MediaObjectList.isEmpty(objects)){
162        for(Iterator<MediaObject> vIter = objects.getMediaObjects().iterator(); vIter.hasNext();){
163          MediaObject object = vIter.next();
164          if(!UserIdentity.equals(userId, object.getOwnerUserId())){
165            LOGGER.warn("Invalid user identity for media object in video, GUID: "+guid);
166            return false;
167          }
168          MediaType mediaType = object.getMediaType();
169          if(!MediaType.VIDEO.equals(mediaType)){
170            LOGGER.debug("Replacing unsupported/incompatible media type: "+mediaType);
171            object.setMediaType(MediaType.VIDEO);
172          }
173          if(object.getVisibility() == null){
174            LOGGER.debug("Object missing visibility value, using video's visibility.");
175            object.setVisibility(visibility);
176          }
177          combined.addMediaObject(object);
178        } // for
179      }
180    }
181    if(!VideoList.isValid(videos)){ // check validity after ids have been generated
182      LOGGER.warn("Tried to add invalid video list.");
183      return false;
184    }
185    SimpleSolrTemplate template = getSolrTemplate(BEAN_ID_SOLR_SERVER); 
186    UpdateResponse response = template.addBeans(v);
187
188    if(response.getStatus() != SolrException.ErrorCode.UNKNOWN.code){
189      LOGGER.warn("Failed to add videos.");
190      return false;
191    }
192
193    if(insert(combined)){
194      associate(videos);
195    }else{
196      LOGGER.warn("Insert failed for combined video list.");
197      return false;
198    }
199    return true;
200  }
201  
202  /**
203   * 
204   * @param authenticatedUser
205   * @param guid
206   * @return access details for the photo, or null if the photo does not exist
207   */
208  public AccessDetails getAccessDetails(UserIdentity authenticatedUser, String guid) {
209    SolrQueryBuilder solr = new SolrQueryBuilder();
210    solr.addFields(FIELDS_GET_ACCESS_DETAILS);
211    solr.addCustomFilter(new AndQueryFilter(SOLR_FIELD_ID, guid));
212
213    List<Video> videos = getSolrTemplate(BEAN_ID_SOLR_SERVER).queryForList(solr.toSolrQuery(Definitions.ELEMENT_VIDEOLIST), Video.class);
214    if(videos == null){
215      LOGGER.debug("GUID does not exist: "+guid);
216      return null;
217    }
218    return AccessDetails.getAccessDetails(authenticatedUser, videos.iterator().next());
219  }
220  
221  /**
222   * 
223   * @param dataGroups optional filter
224   * @param guids optional filter
225   * @param limits optional filter
226   * @param serviceTypes optional filter
227   * @param userIdFilter optional filter
228   * @return list of videos or null if none
229   */
230  public VideoList getVideos(DataGroups dataGroups, Collection<String> guids, Limits limits, EnumSet<ServiceType> serviceTypes, long[] userIdFilter){
231    return getVideoList(dataGroups, guids, limits, serviceTypes, userIdFilter);
232  }
233  
234  /**
235   * 
236   * @param dataGroups
237   * @param guids
238   * @param limits
239   * @param serviceTypes
240   * @param userIdFilter
241   * @return list of videos or null if none was found
242   */
243  private VideoList getVideoList(DataGroups dataGroups, Collection<String> guids, Limits limits, EnumSet<ServiceType> serviceTypes, long[] userIdFilter){
244    SolrQueryBuilder solr = new SolrQueryBuilder(null);
245    if(guids != null && !guids.isEmpty()){
246      LOGGER.debug("Adding GUID filter...");
247      solr.addCustomFilter(new AndQueryFilter(SOLR_FIELD_ID, guids));
248    }
249    if(!ServiceType.isEmpty(serviceTypes)){
250      LOGGER.debug("Adding service type filter...");
251      solr.addCustomFilter(new AndQueryFilter(Definitions.SOLR_FIELD_SERVICE_ID, ServiceType.toIdArray(serviceTypes)));
252    }
253
254    if(!ArrayUtils.isEmpty(userIdFilter)){
255      LOGGER.debug("Adding user id filter...");
256      solr.addCustomFilter(new AndQueryFilter(Definitions.SOLR_FIELD_USER_ID, userIdFilter));
257    }
258
259    solr.setLimits(limits);
260    solr.setSortOptions(DEFAULT_SORT_OPTIONS);
261    setDataGroups(dataGroups, solr);
262
263    QueryResponse response = getSolrTemplate(BEAN_ID_SOLR_SERVER).query(solr.toSolrQuery(Definitions.ELEMENT_VIDEOLIST));
264    List<Video> videos = SimpleSolrTemplate.getList(response, Video.class);
265    if(videos == null){
266      LOGGER.debug("No videos");
267      return null;
268    }
269
270    ResultInfo info = null;
271    if(DataGroups.hasDataGroup(service.tut.pori.contentanalysis.Definitions.DATA_GROUP_RESULT_INFO, dataGroups)){
272      LOGGER.debug("Resolving result info for the requested videos.");
273      info = new ResultInfo(limits.getStartItem(Definitions.ELEMENT_VIDEOLIST), limits.getEndItem(Definitions.ELEMENT_VIDEOLIST), response.getResults().getNumFound());
274    }
275
276    VideoList videoList = VideoList.getVideoList(videos, info);
277    Map<String, Set<String>> guidVoidMap = _associationDAO.getAssociationsForGUIDs(videoList.getGUIDs());
278    if(guidVoidMap == null){
279      LOGGER.debug("No objects for the videos.");
280    }else{
281      for(Entry<String, Set<String>> e : guidVoidMap.entrySet()){
282        MediaObjectList objects = _mediaObjectDAO.getMediaObjects(dataGroups, limits, MEDIA_TYPES, null, e.getValue(), null); // do NOT give serviceTypes as filter, we are searching videos with specific serviceTypes, not mediaObjects
283        if(MediaObjectList.isEmpty(objects)){
284          LOGGER.warn("Could not retrieve objects for guid: "+e.getKey());
285        }else{
286          videoList.getVideo(e.getKey()).addMediaObjects(objects);
287        }
288      }
289    }
290
291    if(DataGroups.hasDataGroup(service.tut.pori.contentanalysis.Definitions.DATA_GROUP_STATUS, dataGroups)){
292      _videoTaskDAO.getMediaStatus(videoList.getVideos());
293    }
294
295    return videoList;
296  }
297  
298  /**
299   * 
300   * @param dataGroups
301   * @param solr
302   */
303  private void setDataGroups(DataGroups dataGroups, SolrQueryBuilder solr){
304    if(DataGroups.hasDataGroup(DataGroups.DATA_GROUP_ALL, dataGroups)){
305      LOGGER.debug("Data group "+DataGroups.DATA_GROUP_ALL+" found, will not set field list.");
306    }else if(DataGroups.hasDataGroup(DataGroups.DATA_GROUP_BASIC, dataGroups)){
307      solr.addFields(FIELDS_DATA_GROUP_BASIC);
308    }else{
309      boolean hasGroup = DataGroups.hasDataGroup(service.tut.pori.contentanalysis.Definitions.DATA_GROUP_VISIBILITY, dataGroups);
310      if(hasGroup){
311        solr.addField(FIELDS_DATA_GROUP_VISIBILITY);
312      }
313
314      if(DataGroups.hasDataGroup(DataGroups.DATA_GROUP_DEFAULTS, dataGroups) || DataGroups.isEmpty(dataGroups)){  // if defaults are explicitly given or there are not datagroups
315        solr.addFields(FIELDS_DATA_GROUP_DEFAULTS);
316      }else if(!hasGroup){
317        LOGGER.debug("No valid data groups, using "+DataGroups.DATA_GROUP_DEFAULTS);
318        solr.addFields(FIELDS_DATA_GROUP_DEFAULTS);
319      }
320    }
321  }
322
323  /**
324   * 
325   * @param authenticatedUser optional filter
326   * @param dataGroups optional filter
327   * @param guids optional filter
328   * @param limits optional filter
329   * @param objects optional filter
330   * @param serviceTypes optional filter
331   * @param userIdFilter optional filter
332   * @return the results, or null if none.
333   * @throws IllegalArgumentException on bad search terms
334   */
335  public VideoList search(UserIdentity authenticatedUser, DataGroups dataGroups, Collection<String> guids, Limits limits, MediaObjectList objects, EnumSet<ServiceType> serviceTypes, long[] userIdFilter) {
336    SolrQueryBuilder solr = new SolrQueryBuilder();
337    setDataGroups(dataGroups, solr);
338    solr.setLimits(limits);
339
340    if(guids != null && !guids.isEmpty()){
341      solr.addCustomFilter(new AndQueryFilter(SOLR_FIELD_ID, guids));
342    }
343
344    Map<String, Set<String>> guidVoidMap = null;
345    if(!MediaObjectList.isEmpty(objects)){ // if media objects have been given as a search term, do a media object look-up first
346      List<String> mediaObjectIds = _mediaObjectDAO.getMediaObjectIds(authenticatedUser, dataGroups, null, null, userIdFilter, objects); // do NOT give serviceTypes as filter, we are searching videos with specific serviceTypes, not mediaObjects
347      if(mediaObjectIds == null){
348        LOGGER.debug("No objects found.");
349        return null;
350      }
351      
352      guidVoidMap = _associationDAO.getAssociationsForMediaObjectIds(mediaObjectIds);
353      if(guidVoidMap == null){
354        LOGGER.debug("No videos associated with the media object results.");
355        return null;
356      }
357      solr.addCustomFilter(new AndQueryFilter(SOLR_FIELD_ID, guidVoidMap.keySet()));
358    }
359
360    if(!ServiceType.isEmpty(serviceTypes)){
361      solr.addCustomFilter(new AndQueryFilter(Definitions.SOLR_FIELD_SERVICE_ID, ServiceType.toIdArray(serviceTypes)));
362    }
363
364    if(!ArrayUtils.isEmpty(userIdFilter)){
365      solr.addCustomFilter(new AndQueryFilter(Definitions.SOLR_FIELD_USER_ID, userIdFilter));
366    }
367
368    if(!UserIdentity.isValid(authenticatedUser)){
369      LOGGER.debug("Invalid authenticated user, limiting search to public content.");
370      solr.addCustomFilter(new AndQueryFilter(Definitions.SOLR_FIELD_VISIBILITY, Visibility.PUBLIC.toInt()));
371    }else{
372      solr.addCustomFilter(new AndSubQueryFilter(new AbstractQueryFilter[]{new OrQueryFilter(Definitions.SOLR_FIELD_USER_ID, authenticatedUser.getUserId()), new OrQueryFilter(Definitions.SOLR_FIELD_VISIBILITY, Visibility.PUBLIC.toInt())}));
373    }
374
375    solr.setSortOptions(DEFAULT_SORT_OPTIONS);
376    QueryResponse response = getSolrTemplate(BEAN_ID_SOLR_SERVER).query(solr.toSolrQuery(Definitions.ELEMENT_VIDEOLIST));
377    List<Video> videos = SimpleSolrTemplate.getList(response, Video.class);
378    if(videos == null){
379      LOGGER.debug("No videos");
380      return null;
381    }
382
383    ResultInfo info = null;
384    if(DataGroups.hasDataGroup(service.tut.pori.contentanalysis.Definitions.DATA_GROUP_RESULT_INFO, dataGroups)){
385      LOGGER.debug("Resolving result info for the requested videos.");
386      info = new ResultInfo(limits.getStartItem(Definitions.ELEMENT_VIDEOLIST), limits.getEndItem(Definitions.ELEMENT_VIDEOLIST), response.getResults().getNumFound());
387    }
388
389    VideoList videoList = VideoList.getVideoList(videos, info);
390    if(guidVoidMap == null){  // resolve media object relations if not resolved already, depending on the data groups given we may not even need the media objects, but let's ignore it for now
391      guidVoidMap = _associationDAO.getAssociationsForGUIDs(videoList.getGUIDs());
392    }
393    
394    if(guidVoidMap == null){
395      LOGGER.debug("No video-media object associations...");
396    }else{
397      LOGGER.debug("Retrieving media objects for the list of videos, if needed...");
398      for(Entry<String, Set<String>> e : guidVoidMap.entrySet()){
399        MediaObjectList videoObject = _mediaObjectDAO.getMediaObjects(dataGroups, limits, MEDIA_TYPES, null, e.getValue(), null); // do NOT give serviceTypes as filter, we are searching videos with specific serviceTypes, not mediaObjects
400        if(MediaObjectList.isEmpty(videoObject)){
401          LOGGER.debug("Could not retrieve objects for GUID: "+e.getKey());
402        }else{
403          Video video = videoList.getVideo(e.getKey());
404          if(video == null){
405            LOGGER.warn("Could not find video, GUID: "+e.getKey());
406          }else{
407            video.addMediaObjects(videoObject);
408          }
409        }
410      }
411    }
412
413    if(DataGroups.hasDataGroup(service.tut.pori.contentanalysis.Definitions.DATA_GROUP_STATUS, dataGroups)){
414      _videoTaskDAO.getMediaStatus(videoList.getVideos());
415    }
416
417    return videoList;
418  }
419
420  /**
421   * Sets the owner details (userId) to the given videos, requires that GUID has been set to the video object
422   * 
423   * @param videos
424   * @return true on success, Note: the failed videos will have userId of null, thus, this method can also be used to check the existence of the given videos.
425   */
426  public boolean setOwners(VideoList videos) {
427    if(VideoList.isEmpty(videos)){
428      LOGGER.debug("Ignored empty "+VideoList.class.toString());
429      return true;
430    }
431
432    List<String> guids = videos.getGUIDs();
433    if(guids == null){
434      LOGGER.debug("No GUIDs.");
435      return false;
436    }
437
438    SolrQueryBuilder solr = new SolrQueryBuilder();
439    solr.addFields(FIELDS_SET_OWNERS);
440    solr.addCustomFilter(new AndQueryFilter(SOLR_FIELD_ID, guids));
441    VideoList found = VideoList.getVideoList(getSolrTemplate(BEAN_ID_SOLR_SERVER).queryForList(solr.toSolrQuery(Definitions.ELEMENT_VIDEOLIST), Video.class),null);
442    if(VideoList.isEmpty(found)){
443      LOGGER.debug("No videos found.");
444      for(Video video : videos.getVideos()){  // null all user ids
445        video.setOwnerUserId(null);
446      }
447    }else{
448      for(Video video : videos.getVideos()){
449        Video foundVideo = found.getVideo(video.getGUID());
450        if(foundVideo != null){
451          video.setOwnerUserId(foundVideo.getOwnerUserId());
452        }else{
453          video.setOwnerUserId(null);
454        }
455      } // for
456    }
457    return true;
458  }
459  
460  /**
461   * create video-media object associations from the given video list
462   * 
463   * @param videos
464   */
465  public void associate(VideoList videos){
466    _associationDAO.associate(videos.getVideos());
467  }
468
469  /**
470   * Note: content added through ContentStorage MUST be removed through ContentStorage, removing the metadata directly using this method may cause undefined behavior.
471   * 
472   * @param guids
473   * @see service.tut.pori.contentstorage.ContentStorageCore
474   */
475  public void remove(Collection<String> guids) {
476    if(guids == null || guids.isEmpty()){
477      LOGGER.debug("Ignored empty GUIDs list.");
478      return;
479    }
480    SimpleSolrTemplate template = getSolrTemplate(BEAN_ID_SOLR_SERVER);
481    if(template.deleteById(guids).getStatus() != SolrException.ErrorCode.UNKNOWN.code){
482      LOGGER.warn("Failed to delete by GUID.");
483    }else{
484      Map<String, Set<String>> guidVoidMap = _associationDAO.getAssociationsForGUIDs(guids);
485      if(guidVoidMap == null){
486        LOGGER.debug("No media objects for the GUID.");
487      }else{
488        for(Entry<String, Set<String>> e : guidVoidMap.entrySet()){
489          if(!_mediaObjectDAO.remove(e.getValue())){ // we do not need to de-associate, this will automatically cleanup the association table (by media object dao)
490            LOGGER.warn("Failed to remove media objects for GUID: "+e.getKey());
491          }
492        }
493      }
494      _videoTaskDAO.remove(guids);
495    }
496  }
497}