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.EnumSet;
019import java.util.Iterator;
020import java.util.List;
021
022import org.apache.commons.lang3.ArrayUtils;
023import org.apache.commons.lang3.StringUtils;
024import org.apache.http.client.methods.HttpPost;
025import org.apache.http.entity.StringEntity;
026import org.apache.log4j.Logger;
027
028import service.tut.pori.contentanalysis.AnalysisBackend;
029import service.tut.pori.contentanalysis.PhotoParameters.AnalysisType;
030import service.tut.pori.contentanalysis.AsyncTask.TaskStatus;
031import service.tut.pori.contentanalysis.AsyncTask.TaskType;
032import service.tut.pori.contentanalysis.BackendStatus;
033import service.tut.pori.contentanalysis.CAContentCore.ServiceType;
034import service.tut.pori.contentanalysis.CAContentCore.Visibility;
035import service.tut.pori.contentanalysis.Definitions;
036import service.tut.pori.contentanalysis.MediaObjectList;
037import service.tut.pori.contentanalysis.reference.CAReferenceCore;
038import service.tut.pori.contentanalysis.reference.CAXMLObjectCreator;
039import service.tut.pori.contentanalysis.video.DeletedVideoList;
040import service.tut.pori.contentanalysis.video.Timecode;
041import service.tut.pori.contentanalysis.video.TimecodeList;
042import service.tut.pori.contentanalysis.video.Video;
043import service.tut.pori.contentanalysis.video.VideoList;
044import service.tut.pori.contentanalysis.video.VideoParameters;
045import service.tut.pori.contentanalysis.video.VideoTaskDetails;
046import service.tut.pori.contentanalysis.video.VideoTaskResponse;
047import core.tut.pori.http.RedirectResponse;
048import core.tut.pori.http.Response;
049import core.tut.pori.http.parameters.DataGroups;
050import core.tut.pori.http.parameters.Limits;
051import core.tut.pori.users.UserIdentity;
052import core.tut.pori.utils.XMLFormatter;
053
054/**
055 * The reference implementations for Video Content Analysis Service.
056 *
057 */
058public final class VideoReferenceCore {
059  private static final VideoXMLObjectCreator CREATOR = new VideoXMLObjectCreator(null);
060  private static final DataGroups DATAGROUPS_BACKEND_RESPONSE = new DataGroups(CAXMLObjectCreator.DATA_GROUP_BACKEND_RESPONSE); // data groups for add task callback
061  private static final Limits DEFAULT_LIMITS = new Limits(0, 0);  // default number of limits for references
062  private static final DataGroups DATAGROUPS_ALL = new DataGroups(DataGroups.DATA_GROUP_ALL);
063  private static final String EXAMPLE_URI = "http://users.ics.aalto.fi/jorma/d2i/hs-00.mp4";
064  private static final Logger LOGGER = Logger.getLogger(VideoReferenceCore.class);
065  
066  /**
067   * 
068   */
069  private VideoReferenceCore(){
070    // nothing needed
071  }
072
073  /**
074   * 
075   * @param authenticatedUser
076   * @param serviceId
077   * @param guid
078   * @return an example response for the given values
079   */
080  public static RedirectResponse generateTargetUrl(UserIdentity authenticatedUser, ServiceType serviceId, String guid) {
081    LOGGER.info((authenticatedUser == null ? "No logged in user." : "Ignoring the logged in user, id: "+authenticatedUser.getUserId()));  // only notify of the logged in status
082    return new RedirectResponse(EXAMPLE_URI);
083  }
084
085  /**
086   * 
087   * @param dataGroups
088   * @return a randomly generated video
089   */
090  public static Video generateVideo(DataGroups dataGroups) {
091    return CREATOR.createVideo(null, (DataGroups.isEmpty(dataGroups) ? DATAGROUPS_ALL : dataGroups), DEFAULT_LIMITS, null, null);
092  }
093
094  /**
095   * 
096   * @param dataGroups
097   * @param limits
098   * @param cls
099   * @return a randomly generated video list
100   */
101  public static VideoList generateVideoList(DataGroups dataGroups, Limits limits, Class<? extends VideoList> cls) {
102    if(VideoList.class == cls){
103      return CREATOR.createVideoList(null, (DataGroups.isEmpty(dataGroups) ? DATAGROUPS_ALL : dataGroups), limits, null, null);
104    }else if(DeletedVideoList.class == cls){
105      return CREATOR.createDeletedVideoList(limits);
106    }else{
107      throw new IllegalArgumentException("Unsupported class : "+cls);
108    }
109  }
110
111  /**
112   * 
113   * @param dataGroups
114   * @param limits
115   * @param taskType
116   * @return randomly generated task details
117   */
118  public static VideoTaskDetails generateVideoTaskDetails(DataGroups dataGroups, Limits limits, TaskType taskType) {
119    switch(taskType){
120      case ANALYSIS:
121      case FEEDBACK:
122        break;
123      default:
124        throw new IllegalArgumentException("Unsupported task type: "+taskType.name());
125    }
126    limits.setTypeLimits(-1, -1, service.tut.pori.contentanalysis.Definitions.ELEMENT_BACKEND_STATUS_LIST); // do not add back-end status list
127    return CREATOR.createVideoTaskDetails(null, (DataGroups.isEmpty(dataGroups) ? DATAGROUPS_ALL : dataGroups), limits, null, taskType);
128  }
129
130  /**
131   * 
132   * @return randomly generated timecode
133   */
134  public static Timecode generateTimecode() {
135    return CREATOR.createTimecode(null);
136  }
137
138  /**
139   * 
140   * @param limits
141   * @return a randomly generated timecode list
142   */
143  public static TimecodeList generateTimecodeList(Limits limits) {
144    return CREATOR.createTimecodeList(limits, false);
145  }
146
147  /**
148   * 
149   * @return randomly generated video options
150   */
151  public static VideoParameters generateVideoOptions() {
152    return CREATOR.createVideoOptions();
153  }
154
155  /**
156   * 
157   * @param limits
158   * @return a randomly generated task response
159   */
160  public static VideoTaskResponse generateTaskResponse(Limits limits) {
161    return CREATOR.createTaskResponse(null, DATAGROUPS_BACKEND_RESPONSE, limits, null, TaskType.ANALYSIS);
162  }
163
164  /**
165   * This performs a trivial check for the task contents, checking for the presence of a few key values.
166   * The validity of the actual task contents will not checked.
167   * 
168   * @param response
169   */
170  public static void taskFinished(VideoTaskResponse response) {
171    Integer tBackendId = response.getBackendId();
172    if(tBackendId == null){
173      throw new IllegalArgumentException("Invalid backendId: "+tBackendId);
174    }
175    Long tTaskId = response.getTaskId();
176    if(tTaskId == null){
177      throw new IllegalArgumentException("Invalid taskId: "+tTaskId);
178    }
179    
180    TaskStatus status = response.getStatus();
181    if(status == null){
182      throw new IllegalArgumentException("TaskStatus is invalid or missing.");
183    }
184
185    TaskType type = response.getTaskType();
186    if(type == null){
187      throw new IllegalArgumentException("TaskType is invalid or missing.");
188    }
189
190    try{
191      switch(type){
192        case ANALYSIS:
193          VideoList vl = response.getVideoList();
194          if(!VideoList.isEmpty(vl)){
195            if(!VideoList.isValid(vl)){
196              LOGGER.warn("Invalid "+service.tut.pori.contentanalysis.video.Definitions.ELEMENT_VIDEOLIST);
197            }
198            for(Video v : response.getVideoList().getVideos()){
199              MediaObjectList vObjects = v.getMediaObjects();
200              if(MediaObjectList.isEmpty(vObjects)){
201                LOGGER.info("No media objects for photo, GUID: "+v.getGUID());
202              }else if(!MediaObjectList.isValid(vObjects)){
203                throw new IllegalArgumentException("Invalid "+service.tut.pori.contentanalysis.Definitions.ELEMENT_MEDIA_OBJECTLIST);
204              }
205            }
206          }
207          break;
208        case FEEDBACK:
209          if(!VideoList.isValid(response.getVideoList())){
210            throw new IllegalArgumentException("Invalid "+service.tut.pori.contentanalysis.video.Definitions.ELEMENT_VIDEOLIST);
211          }
212          break;
213        default:      
214          throw new IllegalArgumentException("Tasks of type: "+type.name()+" are not supported by this validator.");
215      }
216    }catch(ClassCastException ex){
217      LOGGER.debug(ex, ex);
218      throw new IllegalArgumentException("Task content data was not of the expected type.");
219    }
220  }
221
222  /**
223   * 
224   * @param backendId
225   * @param taskId
226   * @param dataGroups
227   * @param limits
228   * @return an example response for the given values
229   */
230  public static Response queryTaskDetails(Integer backendId, Long taskId, DataGroups dataGroups, Limits limits) {
231    if(limits.getMaxItems(service.tut.pori.contentanalysis.video.Definitions.ELEMENT_VIDEOLIST) >= Limits.DEFAULT_MAX_ITEMS){
232      LOGGER.debug("Reseting limits for "+service.tut.pori.contentanalysis.video.Definitions.ELEMENT_VIDEOLIST+": max items was "+Limits.DEFAULT_MAX_ITEMS);
233      limits.setTypeLimits(DEFAULT_LIMITS.getStartItem(service.tut.pori.contentanalysis.video.Definitions.ELEMENT_VIDEOLIST), DEFAULT_LIMITS.getEndItem(service.tut.pori.contentanalysis.video.Definitions.ELEMENT_VIDEOLIST), service.tut.pori.contentanalysis.video.Definitions.ELEMENT_VIDEOLIST); // startItem makes no difference for random
234    }
235    
236    if(limits.getMaxItems(service.tut.pori.contentanalysis.Definitions.ELEMENT_MEDIA_OBJECTLIST) >= Limits.DEFAULT_MAX_ITEMS){
237      LOGGER.debug("Reseting limits for "+service.tut.pori.contentanalysis.Definitions.ELEMENT_MEDIA_OBJECTLIST+": max items was "+Limits.DEFAULT_MAX_ITEMS);
238      limits.setTypeLimits(DEFAULT_LIMITS.getStartItem(service.tut.pori.contentanalysis.Definitions.ELEMENT_MEDIA_OBJECTLIST), DEFAULT_LIMITS.getEndItem(service.tut.pori.contentanalysis.Definitions.ELEMENT_MEDIA_OBJECTLIST), service.tut.pori.contentanalysis.Definitions.ELEMENT_MEDIA_OBJECTLIST); // startItem makes no difference for random
239    }
240    limits.setTypeLimits(-1, -1, null); // disable all other photo lists
241    return new Response(CREATOR.createVideoTaskDetails(backendId, dataGroups, limits, taskId, TaskType.ANALYSIS));
242  }
243
244  /**
245   * 
246   * @param authenticatedUser
247   * @param objects
248   * @param dataGroups
249   * @param limits
250   * @param serviceTypes
251   * @param userIdFilters
252   * @return an example response for the given values
253   */
254  public static Response similarVideosByObject(UserIdentity authenticatedUser, MediaObjectList objects, DataGroups dataGroups, Limits limits, EnumSet<ServiceType> serviceTypes, long[] userIdFilters) {
255    LOGGER.info((authenticatedUser == null ? "No logged in user." : "Ignoring the logged in user, id: "+authenticatedUser.getUserId()));  // only notify of the logged in status
256    if(limits.getMaxItems(service.tut.pori.contentanalysis.video.Definitions.ELEMENT_VIDEOLIST) >= Limits.DEFAULT_MAX_ITEMS){
257      LOGGER.debug("Reseting limits: Default max items was "+Limits.DEFAULT_MAX_ITEMS);
258      limits = DEFAULT_LIMITS; // startItem makes no difference for random
259    }
260    return new Response(CREATOR.createSearchResults(null, dataGroups, limits, serviceTypes, userIdFilters, objects));
261  }
262
263  /**
264   * 
265   * @param authenticatedUser
266   * @param guids
267   * @param dataGroups
268   * @param limits
269   * @param serviceTypes
270   * @param userIdFilter 
271   * @return an example response for the given values
272   */
273  public static Response getVideos(UserIdentity authenticatedUser, List<String> guids, DataGroups dataGroups, Limits limits, EnumSet<ServiceType> serviceTypes, long[] userIdFilter) {
274    LOGGER.info((authenticatedUser == null ? "No logged in user." : "Ignoring the logged in user, id: "+authenticatedUser.getUserId()));  // only notify of the logged in status
275    if(limits.getMaxItems(service.tut.pori.contentanalysis.video.Definitions.ELEMENT_VIDEOLIST) >= Limits.DEFAULT_MAX_ITEMS){
276      LOGGER.debug("Reseting limits: Default max items was "+Limits.DEFAULT_MAX_ITEMS);
277      limits = DEFAULT_LIMITS; // startItem makes no difference for random
278    }
279    int userIdCount = (ArrayUtils.isEmpty(userIdFilter) ? 0 : userIdFilter.length);
280    VideoList list = CREATOR.createVideoList(null, dataGroups, limits, serviceTypes, null);
281    if(list != null && guids != null && !guids.isEmpty()){
282      for(Iterator<Video> iter = list.getVideos().iterator();iter.hasNext();){  // remove all extra guids, we could also modify the limit parameter, but for this testing method, the implementation does not matter
283        Video video = iter.next();
284        if(guids.isEmpty()){  // we have used up all given guids
285          iter.remove();
286        }else{
287          video.setGUID(guids.remove(0));
288          video.setVisibility(Visibility.PUBLIC); // there could also be private photos for the authenticated user, but to make sure the results are valid, return only PUBLIC photos
289        }
290        if(userIdCount > 1){
291          video.setOwnerUserId(new UserIdentity(userIdFilter[CREATOR.getRandom().nextInt(userIdCount)]));
292        }
293      } // for
294    }
295    return new Response(list);
296  }
297  
298  /**
299   * This performs a trivial check for the task contents, checking for the presence of a few key values.
300   * The validity of the actual task contents will not be checked.
301   * 
302   * @param taskDetails
303   */
304  public static void addTask(VideoTaskDetails taskDetails) {
305    Integer tBackendId = taskDetails.getBackendId();
306    if(tBackendId == null){
307      throw new IllegalArgumentException("Invalid backendId: "+tBackendId);
308    }
309    Long tTaskId = taskDetails.getTaskId();
310    if(tTaskId == null){
311      throw new IllegalArgumentException("Invalid taskId: "+tTaskId);
312    }
313
314    String uri = taskDetails.getCallbackUri();
315    if(StringUtils.isBlank(uri)){
316      throw new IllegalArgumentException("Invalid callbackUri: "+uri);
317    }
318
319    TaskType type = taskDetails.getTaskType();
320    if(type == null){
321      throw new IllegalArgumentException("TaskType is invalid or missing.");
322    }
323
324    switch(type){
325      case BACKEND_FEEDBACK:
326        if(!VideoList.isValid(taskDetails.getVideoList())){
327          throw new IllegalArgumentException("Invalid "+service.tut.pori.contentanalysis.video.Definitions.ELEMENT_VIDEOLIST);
328        }
329        addTaskAsyncCallback(taskDetails, null);
330        break;
331      case ANALYSIS:
332        VideoList videoList = taskDetails.getVideoList();
333        if(!VideoList.isValid(videoList)){
334          throw new IllegalArgumentException("Invalid "+service.tut.pori.contentanalysis.video.Definitions.ELEMENT_VIDEOLIST);
335        }
336        if(taskDetails.getDeletedVideoList() != null){
337          throw new IllegalArgumentException(service.tut.pori.contentanalysis.video.Definitions.ELEMENT_DELETED_VIDEOLIST+" cannot appear in a task of type: "+TaskType.ANALYSIS.name());
338        }
339        VideoParameters vp = taskDetails.getTaskParameters();
340        for(Video video : videoList.getVideos()){
341          MediaObjectList mediaObjects = CREATOR.createMediaObjectList((vp == null ? null : vp.getAnalysisTypes()), DATAGROUPS_BACKEND_RESPONSE, DEFAULT_LIMITS, null);
342          for(service.tut.pori.contentanalysis.MediaObject o : mediaObjects.getMediaObjects()){
343            o.setOwnerUserId(null);
344            o.setBackendId(tBackendId);
345            o.setMediaObjectId(null);
346            o.setServiceType(null);
347          }
348          video.addackendStatus(new BackendStatus(new AnalysisBackend(tBackendId), TaskStatus.COMPLETED));
349          video.addMediaObjects(mediaObjects);
350        }
351        addTaskAsyncCallback(taskDetails, videoList);
352        break;
353      case FEEDBACK:
354        if(taskDetails.getVideoList() != null && !VideoList.isValid(taskDetails.getVideoList())){
355          throw new IllegalArgumentException("Invalid "+service.tut.pori.contentanalysis.video.Definitions.ELEMENT_VIDEOLIST);
356        }else if(taskDetails.getDeletedVideoList() == null){
357          throw new IllegalArgumentException(Definitions.ELEMENT_TASK_DETAILS+" requires at least one of "+service.tut.pori.contentanalysis.video.Definitions.ELEMENT_VIDEOLIST+" or "+service.tut.pori.contentanalysis.video.Definitions.ELEMENT_DELETED_VIDEOLIST);
358        }else if(!DeletedVideoList.isValid(taskDetails.getDeletedVideoList())){
359          throw new IllegalArgumentException("Invalid "+service.tut.pori.contentanalysis.video.Definitions.ELEMENT_DELETED_VIDEOLIST);
360        }
361        
362        addTaskAsyncCallback(taskDetails, null);
363        break;
364      default:
365        throw new IllegalArgumentException("Tasks of type: "+type.name()+" are not supported by this validator.");
366    }
367  }
368  
369  /**
370   * 
371   * @param taskId
372   * @param dataGroups
373   * @param limits
374   * @return an example response for the given values
375   */
376  public static Response queryTaskStatus(Long taskId, DataGroups dataGroups, Limits limits) {
377    if(limits.getMaxItems() >= Limits.DEFAULT_MAX_ITEMS){
378      LOGGER.debug("Reseting limits: Default max items was "+Limits.DEFAULT_MAX_ITEMS);
379      limits = DEFAULT_LIMITS; // startItem makes no difference for random
380    }
381    return new Response(CREATOR.createTaskResponse(null, dataGroups, limits, taskId, TaskType.ANALYSIS));
382  }
383
384  /**
385   * Call asynchronously the callback given in the details, returning an example task response
386   * 
387   * @param details
388   * @param videoList
389   * @throws UnsupportedOperationException on unsupported task details
390   * @see service.tut.pori.contentanalysis.video.VideoTaskResponse
391   */
392  public static void addTaskAsyncCallback(VideoTaskDetails details, VideoList videoList) throws UnsupportedOperationException{
393    TaskType type = details.getTaskType();
394    if(!TaskType.ANALYSIS.equals(type)){
395      throw new UnsupportedOperationException("Unsupported task type: "+type);
396    }
397    HttpPost post = new HttpPost(details.getCallbackUri());
398    VideoTaskResponse r = new VideoTaskResponse();
399    r.setBackendId(details.getBackendId());
400    r.setTaskId(details.getTaskId());
401    r.setStatus(TaskStatus.COMPLETED);
402    r.setTaskType(details.getTaskType());
403    r.setVideoList(videoList);
404    post.setEntity(new StringEntity((new XMLFormatter()).toString(r), core.tut.pori.http.Definitions.ENCODING_UTF8));
405    CAReferenceCore.executeAsyncCallback(post);
406  }
407
408  /**
409   * Client API variation of search by GUID
410   * 
411   * @param authenticatedUser
412   * @param analysisTypes 
413   * @param guid
414   * @param dataGroups
415   * @param limits
416   * @param serviceTypes
417   * @param userIdFilters
418   * @return an example response for the given values
419   */
420  public static Response searchSimilarById(UserIdentity authenticatedUser, EnumSet<AnalysisType> analysisTypes, String guid, DataGroups dataGroups, Limits limits, EnumSet<ServiceType> serviceTypes, long[] userIdFilters) {
421    LOGGER.info((authenticatedUser == null ? "No logged in user." : "Ignoring the logged in user, id: "+authenticatedUser.getUserId()));  // only notify of the logged in status
422    return searchSimilarById(analysisTypes, guid, dataGroups, limits, serviceTypes, userIdFilters); // we can directly call the back-end API reference implementation
423  }
424  
425  /**
426   * Back-end API variation of search by GUID
427   * 
428   * @param analysisTypes 
429   * @param serviceTypes
430   * @param guid
431   * @param userIds
432   * @param dataGroups
433   * @param limits
434   * @return an example response for the given values
435   */
436  public static Response searchSimilarById(EnumSet<AnalysisType> analysisTypes, String guid, DataGroups dataGroups, Limits limits, EnumSet<ServiceType> serviceTypes, long[] userIds) {
437    if(limits.getMaxItems() >= Limits.DEFAULT_MAX_ITEMS){
438      LOGGER.debug("Reseting limits: Default max items was "+Limits.DEFAULT_MAX_ITEMS);
439      limits = DEFAULT_LIMITS; // startItem makes no difference for random
440    }
441    return new Response(CREATOR.createSearchResults(guid, dataGroups, limits, serviceTypes, userIds, null));
442  }
443}