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}