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.reference;
017
018import java.util.ArrayList;
019import java.util.Collection;
020import java.util.EnumSet;
021import java.util.Iterator;
022import java.util.List;
023import java.util.Random;
024import java.util.UUID;
025
026import org.apache.commons.lang3.ArrayUtils;
027import org.apache.commons.lang3.RandomStringUtils;
028import org.apache.commons.lang3.StringUtils;
029import org.apache.log4j.Logger;
030
031import service.tut.pori.contentanalysis.PhotoParameters.AnalysisType;
032import service.tut.pori.contentanalysis.AsyncTask.TaskType;
033import service.tut.pori.contentanalysis.BackendStatusList;
034import service.tut.pori.contentanalysis.CAContentCore.ServiceType;
035import service.tut.pori.contentanalysis.CAContentCore.Visibility;
036import service.tut.pori.contentanalysis.Definitions;
037import service.tut.pori.contentanalysis.ResultInfo;
038import service.tut.pori.contentanalysis.MediaObject;
039import service.tut.pori.contentanalysis.MediaObjectList;
040import service.tut.pori.contentanalysis.reference.CAXMLObjectCreator;
041import service.tut.pori.contentanalysis.video.DeletedVideoList;
042import service.tut.pori.contentanalysis.video.Timecode;
043import service.tut.pori.contentanalysis.video.TimecodeList;
044import service.tut.pori.contentanalysis.video.Video;
045import service.tut.pori.contentanalysis.video.VideoList;
046import service.tut.pori.contentanalysis.video.VideoParameters;
047import service.tut.pori.contentanalysis.video.VideoParameters.SequenceType;
048import service.tut.pori.contentanalysis.video.VideoTaskDetails;
049import service.tut.pori.contentanalysis.video.VideoTaskResponse;
050import core.tut.pori.context.ServiceInitializer;
051import core.tut.pori.http.parameters.DataGroups;
052import core.tut.pori.http.parameters.Limits;
053import core.tut.pori.users.UserIdentity;
054import core.tut.pori.utils.MediaUrlValidator.MediaType;
055
056/**
057 * Class that can be used to created example objects/object lists.
058 */
059public class VideoXMLObjectCreator {
060  private static final Limits LIMITS_NO_MEDIA_OBJECTS;
061  static{
062    LIMITS_NO_MEDIA_OBJECTS = new Limits(0, 0);
063    LIMITS_NO_MEDIA_OBJECTS.setTypeLimits(0, -1, Definitions.ELEMENT_MEDIA_OBJECTLIST);
064  }
065  private static final Logger LOGGER = Logger.getLogger(VideoXMLObjectCreator.class);
066  private static final int TEXT_LENGTH = 64;
067  private CAXMLObjectCreator _CACreator = null;
068  
069  /**
070   * 
071   * @param seed for random generator, or null to use default (system time in nanoseconds)
072   */
073  public VideoXMLObjectCreator(Long seed){
074    if(seed == null){
075      seed = System.nanoTime();
076    }
077    _CACreator = new CAXMLObjectCreator(seed);
078  }
079
080  /**
081   * 
082   * @param analysisTypes 
083   * @param dataGroups
084   * @param limits 
085   * @param serviceTypes 
086   * @param userIdentity 
087   * @return a randomly generated video
088   */
089  public Video createVideo(Collection<AnalysisType> analysisTypes, DataGroups dataGroups, Limits limits, EnumSet<ServiceType> serviceTypes, UserIdentity userIdentity) {
090    Video video = new Video();
091    String guid = UUID.randomUUID().toString();
092    video.setGUID(guid);
093    ServiceType serviceType = createVideoServiceType();
094    video.setServiceType(serviceType);
095    UserIdentity userId = (UserIdentity.isValid(userIdentity) ? userIdentity : _CACreator.createUserIdentity());
096    video.setOwnerUserId(userId);
097    
098    int backendStatusCount = (!DataGroups.hasDataGroup(Definitions.DATA_GROUP_BACKEND_STATUS, dataGroups) || limits == null ? 0 : limits.getMaxItems(Definitions.ELEMENT_BACKEND_STATUS_LIST));
099
100    BackendStatusList backendStatus = null;
101    if(DataGroups.hasDataGroup(DataGroups.DATA_GROUP_ALL,dataGroups) || DataGroups.hasDataGroup(DataGroups.DATA_GROUP_BASIC, dataGroups)){
102      video.setCredits(RandomStringUtils.randomAlphabetic(TEXT_LENGTH));
103      video.setName(RandomStringUtils.randomAlphabetic(TEXT_LENGTH));
104      video.setDescription(RandomStringUtils.randomAlphabetic(TEXT_LENGTH));
105      video.setVisibility(_CACreator.createVisibility());
106    }else if(DataGroups.hasDataGroup(CAXMLObjectCreator.DATA_GROUP_BACKEND_RESPONSE, dataGroups)){
107      backendStatus = _CACreator.createBackendStatusContainer(backendStatusCount);
108      video.setBackendStatus(backendStatus);
109    }else if(DataGroups.hasDataGroup(Definitions.DATA_GROUP_VISIBILITY, dataGroups)){
110      video.setVisibility(_CACreator.createVisibility());
111    }
112
113    MediaObjectList mediaObjectList = createMediaObjectList(analysisTypes, dataGroups, limits, serviceTypes);
114    if(!MediaObjectList.isEmpty(mediaObjectList)){
115      for(Iterator<MediaObject> vIter = mediaObjectList.getMediaObjects().iterator(); vIter.hasNext();){  // make sure all the new media objects have the same user identity as the created video
116        vIter.next().setOwnerUserId(userId);
117      }
118      video.setMediaObjects(mediaObjectList);
119    }
120
121    if(DataGroups.hasDataGroup(Definitions.DATA_GROUP_STATUS, dataGroups)){
122      video.setBackendStatus((backendStatus != null ? _CACreator.createBackendStatusContainer(backendStatusCount) : backendStatus));
123    }
124    
125    video.setUrl(generateRedirectUrl(guid, serviceType));
126    
127    return video;
128  }
129  
130  /**
131   * Create media object list using {@link service.tut.pori.contentanalysis.MediaObject}} class.
132   * 
133   * Note that regardless of the given analysis types, this will only return objects of type {@link core.tut.pori.utils.MediaUrlValidator.MediaType#VIDEO} or {@link core.tut.pori.utils.MediaUrlValidator.MediaType#AUDIO}
134   * 
135   * @param analysisTypes  
136   * @param dataGroups
137   * @param limits
138   * @param serviceTypes
139   * @return randomly generated media object list
140   */
141  public MediaObjectList createMediaObjectList(Collection<AnalysisType> analysisTypes, DataGroups dataGroups, Limits limits, EnumSet<ServiceType> serviceTypes) {
142    MediaObjectList objects = _CACreator.createMediaObjectList(analysisTypes, dataGroups, limits, serviceTypes);
143    if(!MediaObjectList.isEmpty(objects) && (DataGroups.hasDataGroup(Definitions.DATA_GROUP_TIMECODES, dataGroups) || DataGroups.hasDataGroup(DataGroups.DATA_GROUP_ALL, dataGroups))){
144      boolean hasAudio = (analysisTypes != null && analysisTypes.contains(AnalysisType.AUDIO));
145      boolean hasVideo = (!hasAudio || analysisTypes.contains(AnalysisType.VISUAL));
146      Random r = _CACreator.getRandom();
147      for(MediaObject vo : objects.getMediaObjects()){
148        if(hasAudio){
149          if(hasVideo && r.nextBoolean()){
150            vo.setMediaType(MediaType.VIDEO);
151          }else{
152            vo.setMediaType(MediaType.AUDIO);
153          }
154        }else{
155          vo.setMediaType(MediaType.VIDEO);
156        }
157        vo.setTimecodes(createTimecodeList(limits, false));
158      }
159    }
160    return objects;
161  }
162
163  /**
164   * 
165   * @param guid
166   * @param type
167   * @return redirection URL for the given GUID and type or null if either one the given values was null
168   */
169  public String generateRedirectUrl(String guid, ServiceType type){
170    if(type == null || StringUtils.isBlank(guid)){
171      LOGGER.error("GUID or service type was null.");
172      return null;
173    }
174    return ServiceInitializer.getPropertyHandler().getRESTBindContext()+service.tut.pori.contentanalysis.video.reference.Definitions.SERVICE_VCA_REFERENCE_CLIENT+"/"+service.tut.pori.contentanalysis.Definitions.METHOD_REDIRECT+"?"+service.tut.pori.contentanalysis.Definitions.PARAMETER_GUID+"="+guid+"&"+service.tut.pori.contentanalysis.Definitions.PARAMETER_SERVICE_ID+"="+type.getServiceId();
175  }
176  
177  /**
178   * 
179   * @return serviceType valid for videos (not facebook jazz)
180   */
181  public ServiceType createVideoServiceType(){
182    return ServiceType.PICASA_STORAGE_SERVICE;
183  }
184
185  /**
186   * @param analysisTypes 
187   * @param dataGroups
188   * @param limits 
189   * @param serviceTypes
190   * @return randomly generated media object
191   */
192  public MediaObject createVideoMediaObject(Collection<AnalysisType> analysisTypes, DataGroups dataGroups, Limits limits, EnumSet<ServiceType> serviceTypes) {
193    MediaObject vvo = _CACreator.createMediaObject(analysisTypes, dataGroups, serviceTypes);
194    vvo.setMediaType(MediaType.VIDEO);
195    vvo.setTimecodes(createTimecodeList(limits, false));
196    return vvo;
197  }
198
199  /**
200   * 
201   * @param previousEnd if not null, the start time will be after this time
202   * @return randomly generated timecode
203   */
204  public Timecode createTimecode(Double previousEnd) {
205    Timecode timecode = new Timecode();
206    Random r = _CACreator.getRandom();
207    double start = r.nextDouble()*r.nextInt(3600);
208    if(previousEnd != null){
209      start += previousEnd;
210    }
211    timecode.setStart(start);
212    timecode.setEnd(start+r.nextDouble()*r.nextInt(3600));
213    return timecode;
214  }
215
216  /**
217   * 
218   * @param limits
219   * @param sequential if true the timecodes will appear in sequential order, otherwise they are random and may cover duplicate time periods
220   * @return a randomly generated time code list
221   */
222  public TimecodeList createTimecodeList(Limits limits, boolean sequential) {
223    int count = (limits == null ? 0 : limits.getMaxItems(service.tut.pori.contentanalysis.video.Definitions.ELEMENT_TIMECODELIST));
224    if(count < 1){
225      LOGGER.warn("count < 1");
226      return null;
227    }else if(count >= Limits.DEFAULT_MAX_ITEMS){
228      LOGGER.debug("Count was "+Limits.DEFAULT_MAX_ITEMS+", using 1.");
229      count = 1;
230    }
231
232    TimecodeList tcl = new TimecodeList();
233    Double previousEnd = null;
234    for(int i=0;i<count;++i){
235      Timecode tc = createTimecode(previousEnd);
236      if(sequential){
237        previousEnd = tc.getEnd();
238      }
239      tcl.addTimecode(tc);
240    }
241    return tcl;
242  }
243
244  /**
245   * 
246   * @param analysisTypes 
247   * @param dataGroups
248   * @param limits
249   * @param serviceTypes
250   * @param userIdentity 
251   * @return randomly generated video list
252   */
253  public VideoList createVideoList(Collection<AnalysisType> analysisTypes, DataGroups dataGroups, Limits limits, EnumSet<ServiceType> serviceTypes, UserIdentity userIdentity) {
254    int count = limits.getMaxItems(service.tut.pori.contentanalysis.video.Definitions.ELEMENT_VIDEOLIST);
255    if(count < 1){
256      LOGGER.warn("count < 1");
257      return null;
258    }else if(count >= Limits.DEFAULT_MAX_ITEMS){
259      LOGGER.debug("Count was "+Limits.DEFAULT_MAX_ITEMS+", using 1.");
260      count = 1;
261    }
262    VideoList list = new VideoList();
263    for(int i=0;i<count;++i){
264      list.addVideo(createVideo(analysisTypes, dataGroups, limits, serviceTypes, userIdentity));
265    }
266    if(DataGroups.hasDataGroup(Definitions.DATA_GROUP_RESULT_INFO, dataGroups)){
267      list.setResultInfo(new ResultInfo(limits.getStartItem(service.tut.pori.contentanalysis.video.Definitions.ELEMENT_VIDEOLIST), limits.getEndItem(service.tut.pori.contentanalysis.video.Definitions.ELEMENT_VIDEOLIST), count));
268    }
269    return list;
270  }
271
272  /**
273   * 
274   * @param limits
275   * @return randomly generated deleted video list
276   */
277  public DeletedVideoList createDeletedVideoList(Limits limits) {
278    int count = limits.getMaxItems(service.tut.pori.contentanalysis.video.Definitions.ELEMENT_DELETED_VIDEOLIST);
279    if(count < 1){
280      LOGGER.warn("count < 1");
281      return null;
282    }else if(count >= Limits.DEFAULT_MAX_ITEMS){
283      LOGGER.debug("Count was "+Limits.DEFAULT_MAX_ITEMS+", using 1.");
284      count = 1;
285    }
286    DeletedVideoList list = new DeletedVideoList();
287    for(int i=0;i<count;++i){
288      list.addVideo(createVideo(null, null, LIMITS_NO_MEDIA_OBJECTS, null, null));
289    }
290    return list;
291  }
292
293  /**
294   * Create video options for an analysis task.
295   * 
296   * @return randomly generated options
297   */
298  public VideoParameters createVideoOptions() {
299    VideoParameters options = new VideoParameters();
300    SequenceType t = getSequenceType();
301    options.setSequenceType(t);
302    Random r = _CACreator.getRandom();
303    switch(t){
304      case SECOND:
305        options.setSequenceDuration(Math.abs(r.nextInt()));
306        break;
307      case FRAME: // do not generate duration if frame-based analysis is chosen
308      case FULL: // do not generate duration if entire video is chosen
309      case SHOT: // do not generate duration if variable-length shots are chosen
310        break;
311      default:
312        throw new UnsupportedOperationException("Unhandelled "+SequenceType.class.toString()+" : "+t.name());
313    }
314    
315    if(r.nextBoolean()){
316      options.setTimecodes(createTimecodeList(new Limits(0,r.nextInt(5)), true));
317    }
318    options.setAnalysisTypes(EnumSet.allOf(AnalysisType.class));
319    return options;
320  }
321  
322  /**
323   * 
324   * @return random sequence type
325   */
326  public SequenceType getSequenceType() {
327    Random r = _CACreator.getRandom();
328    SequenceType[] types = SequenceType.values();
329    return types[r.nextInt(types.length)];
330  }
331
332  /**
333   * 
334   * @param analysisTypes 
335   * @param dataGroups
336   * @param limits
337   * @param taskId if null, value is randomly generated
338   * @param taskType if null, value is randomly generated
339   * @return randomly generated task response
340   */
341  public VideoTaskResponse createTaskResponse(Collection<AnalysisType> analysisTypes, DataGroups dataGroups, Limits limits, Long taskId, TaskType taskType) {
342    VideoTaskResponse response = new VideoTaskResponse();
343    Random r = _CACreator.getRandom();
344    response.setTaskId((taskId == null ? Math.abs(r.nextLong()) : taskId));
345    Integer backendId = Math.abs(r.nextInt());
346    response.setBackendId(backendId);
347    response.setMessage(RandomStringUtils.randomAlphabetic(TEXT_LENGTH));
348    response.setTaskType((taskType == null ? _CACreator.createTaskType() : taskType));
349    response.setStatus(_CACreator.createTaskStatus());
350    VideoList videoList = createVideoList(analysisTypes, dataGroups, limits, null, _CACreator.createUserIdentity());
351    if(!VideoList.isEmpty(videoList)){
352      for(Video v : videoList.getVideos()){ // make sure all media objects have the same back-end id as the task has
353        MediaObjectList mediaObjectList = v.getMediaObjects();
354        if(!MediaObjectList.isEmpty(mediaObjectList)){
355          for(MediaObject vo : mediaObjectList.getMediaObjects()){
356            vo.setBackendId(backendId);
357          }
358        }
359      }
360      response.setVideoList(videoList);
361    }
362    return response;
363  }
364
365  /**
366   * @param backendId if null, the value is randomly generated
367   * @param dataGroups
368   * @param limits
369   * @param taskId if null, the value is randomly generated
370   * @param taskType ANALYSIS or FEEDBACK, if null, type is chosen randomly
371   * @return randomly generated video task details
372   * @throws UnsupportedOperationException on bad type
373   */
374  public VideoTaskDetails createVideoTaskDetails(Integer backendId, DataGroups dataGroups, Limits limits, Long taskId, TaskType taskType) {
375    if(taskType == null){
376      taskType = createVideoTaskDetailsType();
377    }
378    VideoTaskDetails details = new VideoTaskDetails(taskType);
379    UserIdentity userIdentity = _CACreator.createUserIdentity();
380    Random r = _CACreator.getRandom();
381    switch(taskType){
382      case ANALYSIS:
383        details.setTaskParameters(createVideoOptions());
384        details.setVideoList(createVideoList(null, dataGroups, limits, null, userIdentity));
385        break;
386      case FEEDBACK:
387        if(r.nextBoolean()){  // create randomly deleted feedback
388          details.setDeletedVideoList(createDeletedVideoList(limits));
389        }else{  // normal video list
390          details.setVideoList(createVideoList(null, dataGroups, limits, null, userIdentity));
391        }
392        break;
393      default:
394        throw new UnsupportedOperationException("Unsupported type: "+taskType.name());
395    }
396    
397    details.setBackendId((backendId == null ? Math.abs(r.nextInt()) : backendId));
398    details.setTaskId((taskId == null ? Math.abs(r.nextLong()) : taskId));
399    details.setBackends(_CACreator.createBackendStatusContainer(limits.getMaxItems(Definitions.ELEMENT_BACKEND_STATUS_LIST)));
400    details.setUserId(userIdentity);
401    details.setCallbackUri(generateFinishedCallbackUri()); // override the default uri
402    return details;
403  }
404  
405  /**
406   * 
407   * @return the default task finished callback uri
408   */
409  public String generateFinishedCallbackUri(){
410    return ServiceInitializer.getPropertyHandler().getRESTBindContext()+service.tut.pori.contentanalysis.video.reference.Definitions.SERVICE_VCA_REFERENCE_SERVER+"/"+service.tut.pori.contentanalysis.Definitions.METHOD_TASK_FINISHED;
411  }
412  
413  /**
414   * 
415   * @return randomly generated task type
416   */
417  public TaskType createVideoTaskDetailsType(){
418    return (_CACreator.getRandom().nextBoolean() ? TaskType.ANALYSIS : TaskType.FEEDBACK);
419  }
420
421  /**
422   * Create example search results, making sure that one of the given userIds, mediaObjects and serviceTypes is
423   * set for the videos, if userIds, mediaObjects or serviceTypes is null or empty, a random value will be generated. Note:
424   * for videos with visibility PUBLIC, any userId is OK, and the given user id will not necessarily be set.
425   * If GUID is given, the first video in the list (when the given limits permit it) will have the given GUID.
426   * @param guid
427   * @param dataGroups
428   * @param limits
429   * @param serviceTypes
430   * @param userIds
431   * @param mediaObjects
432   * @return  randomly generated video list
433   */
434  public VideoList createSearchResults(String guid, DataGroups dataGroups, Limits limits, EnumSet<ServiceType> serviceTypes, long[] userIds, MediaObjectList mediaObjects) {
435    VideoList list = createVideoList(null, dataGroups, limits, serviceTypes, null);
436    if(list == null){
437      return null;
438    }
439    
440    boolean hasUserIds = !ArrayUtils.isEmpty(userIds);
441
442    List<ServiceType> types = null;
443    int serviceTypeCount = 0;
444    if(serviceTypes != null && !serviceTypes.isEmpty()){
445      types = new ArrayList<>(serviceTypes);
446      serviceTypeCount = serviceTypes.size();
447    }
448    
449    int mediaObjectCount = 0;
450    if(!MediaObjectList.isEmpty(mediaObjects)){
451      mediaObjectCount = mediaObjects.getMediaObjects().size();
452    }
453    
454    if(!hasUserIds && types == null && mediaObjectCount < 1){
455      LOGGER.debug("No userIds, mediaObjects or types.");
456      return list;
457    }
458    
459    Random r = _CACreator.getRandom();
460    for(Video video : list.getVideos()){
461      if(guid != null){ // if uid has been given, and has not been set already
462        video.setGUID(guid);
463        guid = null;
464      }
465      if(hasUserIds){ 
466        if(!Visibility.PUBLIC.equals(video.getVisibility())){ // only change for photos that do not have public visibility
467          video.setOwnerUserId(new UserIdentity(userIds[r.nextInt(userIds.length)]));
468        }
469      }else{
470        video.setVisibility(Visibility.PUBLIC); // there could also be photos with visibility PRIVATE, if the user was logged in, but setting all to PUBLIC will ensure that the example result is valid
471      }
472      if(types != null){
473        video.setServiceType(types.get(r.nextInt(serviceTypeCount))); // make sure there are service types only from the given set
474      }
475      if(mediaObjectCount > 0){
476        List<MediaObject> objects = video.getMediaObjects().getMediaObjects();
477        if(objects != null){  // don't do anything if there are no other objects (probably filtered out by limits or datagroups)
478          objects.remove(0);  // remove one just in case, to keep the limits...
479          video.addMediaObject(mediaObjects.getMediaObjects().get(r.nextInt(mediaObjectCount))); // ...and add new one for the removed one
480        }
481      }
482    }
483    return list;
484  }
485
486  /**
487   * @return the random generator used for this object creator
488   * @see service.tut.pori.contentanalysis.reference.CAXMLObjectCreator#getRandom()
489   */
490  public Random getRandom() {
491    return _CACreator.getRandom();
492  }
493}