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.contentanalysis; 017 018import java.util.Collection; 019import java.util.EnumSet; 020import java.util.Iterator; 021import java.util.List; 022 023import org.apache.commons.lang3.StringUtils; 024import org.apache.log4j.Logger; 025import org.quartz.JobExecutionContext; 026import org.quartz.JobExecutionException; 027 028import service.tut.pori.contentanalysis.AnalysisBackend.Capability; 029import core.tut.pori.context.ServiceInitializer; 030import core.tut.pori.users.UserIdentity; 031 032 033/** 034 * An implementation of ASyncTask, meant for executing a feedback task. 035 * 036 * Requires a valid taskId for execution, provided in a JobExecutionContext. 037 * 038 */ 039public class PhotoFeedbackTask extends AsyncTask{ 040 private static final Logger LOGGER = Logger.getLogger(PhotoFeedbackTask.class); 041 042 /** 043 * 044 * @param response 045 * @throws IllegalArgumentException 046 */ 047 public static void taskFinished(PhotoTaskResponse response) throws IllegalArgumentException{ 048 Integer backendId = response.getBackendId(); 049 Long taskId = response.getTaskId(); 050 051 PhotoTaskDAO taskDAO = ServiceInitializer.getDAOHandler().getSQLDAO(PhotoTaskDAO.class); 052 BackendStatus taskStatus = taskDAO.getBackendStatus(backendId, taskId); 053 if(taskStatus == null){ 054 LOGGER.warn("Backend, id: "+backendId+" returned results for task, not given to the backend. TaskId: "+taskId); 055 throw new IllegalArgumentException("This task is not given for backend, id: "+backendId); 056 } 057 058 TaskStatus status = response.getStatus(); 059 if(status == null){ 060 LOGGER.warn("Task status not available."); 061 status = TaskStatus.UNKNOWN; 062 } 063 taskStatus.setStatus(status); 064 065 try{ 066 PhotoList results = response.getPhotoList(); 067 if(PhotoList.isEmpty(results)){ 068 LOGGER.warn("No results returned by the backendId: "+backendId); 069 return; 070 } 071 072 if(!PhotoList.isValid(results)){ 073 LOGGER.warn("Invalid photoList."); 074 } 075 076 PhotoDAO photoDAO = ServiceInitializer.getDAOHandler().getSolrDAO(PhotoDAO.class); 077 if(!photoDAO.setOwners(results)){ 078 LOGGER.warn("Could not get owner information for all photos."); 079 } 080 081 PhotoList associations = new PhotoList(); 082 MediaObjectList insert = new MediaObjectList(); 083 MediaObjectList update = new MediaObjectList(); 084 MediaObjectDAO vdao = ServiceInitializer.getDAOHandler().getSolrDAO(MediaObjectDAO.class); 085 for(Iterator<Photo> photoIter = results.getPhotos().iterator(); photoIter.hasNext();){ 086 Photo photo = photoIter.next(); 087 String guid = photo.getGUID(); 088 UserIdentity userId = photo.getOwnerUserId(); 089 if(!UserIdentity.isValid(userId)){ // if this photo does not exist, there won't be userId 090 LOGGER.warn("Ignoring non-existing photo, GUID: "+guid+" from backend, id: "+backendId); 091 continue; 092 } 093 BackendStatusList c = photo.getBackendStatus(); 094 if(BackendStatusList.isEmpty(c)){ 095 LOGGER.debug("Backend status not available for photo, GUID: "+guid); 096 }else if(c.getCombinedStatus() == TaskStatus.ERROR){ 097 LOGGER.warn("Error condition detected for photo, GUID: "+guid); 098 }else{ 099 List<BackendStatus> sList = c.getBackendStatuses(); 100 if(sList.size() > 1){ 101 status = TaskStatus.ERROR; 102 throw new IllegalArgumentException("Multiple backend statuses."); 103 } 104 if(!backendId.equals(sList.get(0).getBackendId())){ 105 status = TaskStatus.ERROR; 106 throw new IllegalArgumentException("Invalid backend status."); 107 } 108 } 109 MediaObjectList vObjects = photo.getMediaObjects(); 110 if(!MediaObjectList.isEmpty(vObjects)){ // make sure all objects have proper user 111 for(MediaObject mediaObject : vObjects.getMediaObjects()){ // check that the objects are valid 112 if(!backendId.equals(mediaObject.getBackendId())){ 113 LOGGER.warn("Task backend id "+backendId+" does not match the backend id "+mediaObject.getBackendId()+" given for media object, objectId: "+mediaObject.getObjectId()); 114 mediaObject.setBackendId(backendId); 115 } 116 mediaObject.setOwnerUserId(userId); 117 } 118 vdao.resolveObjectIds(vObjects); // resolve ids for update/insert sort 119 Photo iPhoto = null; 120 for(MediaObject vo : vObjects.getMediaObjects()){ // re-sort to to updated and new 121 if(StringUtils.isBlank(vo.getMediaObjectId())){ // no media object id, this is a new one 122 if(iPhoto == null){ 123 associations.getPhoto(guid); // get target photo for insertion 124 if(iPhoto == null){ 125 iPhoto = new Photo(guid); 126 associations.addPhoto(iPhoto); 127 } 128 } 129 iPhoto.addMediaObject(vo); 130 insert.addMediaObject(vo); 131 }else{ 132 update.addMediaObject(vo); 133 } 134 } // for 135 } // for objects 136 } 137 138 if(MediaObjectList.isEmpty(insert)){ 139 LOGGER.debug("Nothing to insert."); 140 }else if(!MediaObjectList.isValid(insert)){ 141 status = TaskStatus.ERROR; 142 throw new IllegalArgumentException("Invalid media object list."); 143 }else if(!photoDAO.insert(insert)){ 144 LOGGER.warn("Failed to insert new objects."); 145 }else{ 146 photoDAO.associate(associations); 147 } 148 149 if(MediaObjectList.isEmpty(update)){ 150 LOGGER.debug("Nothing to update"); 151 }else if(!MediaObjectList.isValid(update)){ 152 status = TaskStatus.ERROR; 153 throw new IllegalArgumentException("Invalid media object list."); 154 }else if(!photoDAO.update(update)){ 155 LOGGER.warn("Failed to update objects."); 156 } 157 158 taskDAO.updateMediaStatus(results.getPhotos(), taskId); 159 taskDAO.updateTaskStatus(taskStatus, taskId); 160 } finally { 161 ServiceInitializer.getEventHandler().publishEvent(new AsyncTaskEvent(backendId, PhotoFeedbackTask.class, status, taskId, TaskType.FEEDBACK)); 162 } 163 } 164 165 @Override 166 public void execute(JobExecutionContext context) throws JobExecutionException { 167 executeAddTask(EnumSet.of(Capability.PHOTO_ANALYSIS, Capability.USER_FEEDBACK), ServiceInitializer.getDAOHandler().getSQLDAO(PhotoTaskDAO.class), getTaskId(context.getMergedJobDataMap())); 168 } 169 170 /** 171 * A helper class building PhotoTaskDetails usable with {@link PhotoFeedbackTask} and executable using {@link service.tut.pori.contentanalysis.CAContentCore#scheduleTask(PhotoTaskDetails)}} 172 * @see service.tut.pori.contentanalysis.CAContentCore 173 * @see service.tut.pori.contentanalysis.PhotoTaskDetails 174 */ 175 public static class FeedbackTaskBuilder{ 176 private PhotoTaskDetails _details = null; 177 178 /** 179 * for sub-classing 180 */ 181 protected FeedbackTaskBuilder(){ 182 // nothing needed 183 } 184 185 /** 186 * 187 * @param taskType {@link service.tut.pori.contentanalysis.AsyncTask.TaskType#FEEDBACK} 188 * @throws IllegalArgumentException on unsupported/invalid task type 189 */ 190 public FeedbackTaskBuilder(TaskType taskType) throws IllegalArgumentException { 191 if(taskType != TaskType.FEEDBACK){ 192 throw new IllegalArgumentException("Invalid task type."); 193 } 194 _details = new PhotoTaskDetails(taskType); 195 } 196 197 /** 198 * Add photo to feedback task if the given photo has (valid) changes 199 * 200 * @param photo 201 * @return this 202 */ 203 public FeedbackTaskBuilder addPhoto(Photo photo){ 204 if(photo == null){ 205 LOGGER.warn("Ignored null photo."); 206 }else{ 207 _details.addPhoto(photo); 208 } 209 return this; 210 } 211 212 /** 213 * 214 * @param photos 215 * @return this 216 */ 217 public FeedbackTaskBuilder addPhotos(PhotoList photos){ 218 if(PhotoList.isEmpty(photos)){ 219 LOGGER.warn("Ignored empty photo list."); 220 }else{ 221 for(Photo p : photos.getPhotos()){ 222 addPhoto(p); 223 } 224 } 225 return this; 226 } 227 228 /** 229 * Set this photo as the reference photo 230 * 231 * @param photo 232 * @return this 233 * @throws IllegalArgumentException if the given photo is already present in similar or dissimilar photo list 234 */ 235 public FeedbackTaskBuilder addReferencePhoto(Photo photo) throws IllegalArgumentException{ 236 if(photo == null){ 237 LOGGER.warn("Ignored null photo."); 238 return this; 239 } 240 String guid = photo.getGUID(); 241 if(StringUtils.isEmpty(guid)){ 242 throw new IllegalArgumentException("No GUID for the given photo."); 243 } 244 245 SimilarPhotoList similar = _details.getSimilarPhotoList(); 246 if(!PhotoList.isEmpty(similar) && similar.getPhoto(guid) != null){ 247 throw new IllegalArgumentException("Same photo cannot appear in reference list and similar photo list."); 248 } 249 DissimilarPhotoList dissimilar = _details.getDissimilarPhotoList(); 250 if(!PhotoList.isEmpty(dissimilar) && dissimilar.getPhoto(guid) != null){ 251 throw new IllegalArgumentException("Same photo cannot appear in reference list and dissimilar photo list."); 252 } 253 _details.addReferencePhoto(photo); 254 return this; 255 } 256 257 /** 258 * 259 * @param photo 260 * @return this 261 * @throws IllegalArgumentException if the given photo is already present in reference or dissimilar photo list 262 */ 263 public FeedbackTaskBuilder addSimilarPhoto(Photo photo) throws IllegalArgumentException{ 264 if(photo == null){ 265 LOGGER.warn("Ignored null photo."); 266 return this; 267 } 268 269 String guid = photo.getGUID(); 270 if(StringUtils.isEmpty(guid)){ 271 throw new IllegalArgumentException("No GUID for the given photo."); 272 } 273 274 ReferencePhotoList references = _details.getReferencePhotoList(); 275 if(!PhotoList.isEmpty(references) && references.getPhoto(guid) != null){ 276 throw new IllegalArgumentException("Same photo cannot appear in reference list and similar photo list."); 277 } 278 DissimilarPhotoList dissimilar = _details.getDissimilarPhotoList(); 279 if(!PhotoList.isEmpty(dissimilar) && dissimilar.getPhoto(guid) != null){ 280 throw new IllegalArgumentException("Same photo cannot appear in similar list and dissimilar photo list."); 281 } 282 _details.addSimilarPhoto(photo); 283 return this; 284 } 285 286 /** 287 * 288 * @param photo 289 * @return this 290 * @throws IllegalArgumentException if the given photo is already present in reference or similar photo list 291 */ 292 public FeedbackTaskBuilder addDissimilarPhoto(Photo photo) throws IllegalArgumentException{ 293 if(photo == null){ 294 LOGGER.warn("Ignored invalid photo."); 295 return this; 296 } 297 298 String guid = photo.getGUID(); 299 if(StringUtils.isEmpty(guid)){ 300 throw new IllegalArgumentException("No GUID for the given photo."); 301 } 302 303 ReferencePhotoList references = _details.getReferencePhotoList(); 304 if(!PhotoList.isEmpty(references) && references.getPhoto(guid) != null){ 305 throw new IllegalArgumentException("Same photo cannot appear in reference list and similar photo list."); 306 } 307 SimilarPhotoList similar = _details.getSimilarPhotoList(); 308 if(!PhotoList.isEmpty(similar) && similar.getPhoto(guid) != null){ 309 throw new IllegalArgumentException("Same photo cannot appear in similar list and dissimilar photo list."); 310 } 311 _details.addDissimilarPhoto(photo); 312 return this; 313 } 314 315 /** 316 * 317 * @param photo 318 * @return this 319 * @throws IllegalArgumentException 320 */ 321 public FeedbackTaskBuilder addDeletedPhoto(Photo photo) throws IllegalArgumentException{ 322 if(photo == null){ 323 LOGGER.warn("Ignored null photo."); 324 return this; 325 }else if(StringUtils.isBlank(photo.getGUID())){ 326 throw new IllegalArgumentException("No GUID."); 327 } 328 _details.addDeletedPhoto(photo); 329 return this; 330 } 331 332 /** 333 * 334 * @param guids 335 * @return this 336 */ 337 public FeedbackTaskBuilder addDeletedPhotos(Collection<String> guids){ 338 if(guids == null || guids.isEmpty()){ 339 LOGGER.warn("Ignored empty deleted photo list."); 340 return this; 341 } 342 for(String guid : guids){ 343 addDeletedPhoto(new Photo(guid)); 344 } 345 return this; 346 } 347 348 /** 349 * 350 * @param photos 351 * @return this 352 */ 353 public FeedbackTaskBuilder addDeletedPhotos(DeletedPhotoList photos){ 354 if(DeletedPhotoList.isEmpty(photos)){ 355 LOGGER.warn("Ignored empty deleted photo list."); 356 return this; 357 } 358 DeletedPhotoList deleted = _details.getDeletedPhotoList(); 359 if(DeletedPhotoList.isEmpty(deleted)){ 360 _details.setDeletedPhotoList(photos); 361 }else{ 362 deleted.addPhotos(photos); 363 } 364 return this; 365 } 366 367 /** 368 * 369 * @param photos 370 * @return this 371 */ 372 public FeedbackTaskBuilder addDissimilarPhotos(SimilarPhotoList photos){ 373 if(SimilarPhotoList.isEmpty(photos)){ 374 LOGGER.warn("Ignored empty similar photo list."); 375 return this; 376 } 377 for(Photo photo : photos.getPhotos()){ 378 addSimilarPhoto(photo); 379 } 380 return this; 381 } 382 383 /** 384 * 385 * @param photos 386 * @return this 387 */ 388 public FeedbackTaskBuilder addSimilarPhotos(DissimilarPhotoList photos){ 389 if(DissimilarPhotoList.isEmpty(photos)){ 390 LOGGER.warn("Ignored empty dissimilar photo list."); 391 return this; 392 } 393 for(Photo photo : photos.getPhotos()){ 394 addDissimilarPhoto(photo); 395 } 396 return this; 397 } 398 399 /** 400 * 401 * @param userId 402 * @return this 403 */ 404 public FeedbackTaskBuilder setUser(UserIdentity userId){ 405 _details.setUserId(userId); 406 return this; 407 } 408 409 /** 410 * 411 * @param confidence 412 * @return this 413 */ 414 public FeedbackTaskBuilder setUserConfidence(Double confidence){ 415 _details.setUserConfidence(confidence); 416 return this; 417 } 418 419 /** 420 * 421 * @param end 422 * @return this 423 * @throws IllegalArgumentException on null or invalid back-end 424 */ 425 public FeedbackTaskBuilder addBackend(AnalysisBackend end) throws IllegalArgumentException{ 426 if(end == null || !end.hasCapability(Capability.USER_FEEDBACK)){ 427 throw new IllegalArgumentException("The given back-end, id: "+end.getBackendId()+" does not have the required capability: "+Capability.USER_FEEDBACK.name()); 428 } 429 _details.setBackend(new BackendStatus(end, TaskStatus.NOT_STARTED)); 430 return this; 431 } 432 433 /** 434 * This will automatically filter out back-end with inadequate capabilities 435 * 436 * @param backendStatusList 437 * @return this 438 */ 439 public FeedbackTaskBuilder setBackends(BackendStatusList backendStatusList){ 440 if(BackendStatusList.isEmpty(backendStatusList)){ 441 LOGGER.warn("Empty backend status list."); 442 backendStatusList = null; 443 }else if((backendStatusList = BackendStatusList.getBackendStatusList(backendStatusList.getBackendStatuses(EnumSet.of(Capability.USER_FEEDBACK)))) == null){ // filter out back-ends with invalid capabilities 444 LOGGER.warn("List contains no back-ends with valid capability "+Capability.USER_FEEDBACK.name()+"for task type "+TaskType.FEEDBACK.name()); 445 } 446 _details.setBackends(backendStatusList); 447 return this; 448 } 449 450 /** 451 * 452 * @return this 453 */ 454 public FeedbackTaskBuilder clearDeletedPhotos(){ 455 _details.setDeletedPhotoList(null); 456 return this; 457 } 458 459 /** 460 * 461 * @return this 462 */ 463 public FeedbackTaskBuilder clearSimilarPhotos(){ 464 _details.setSimilarPhotoList(null); 465 return this; 466 } 467 468 /** 469 * 470 * @return this 471 */ 472 public FeedbackTaskBuilder clearDissimilarPhotos(){ 473 _details.setDissimilarPhotoList(null); 474 return this; 475 } 476 477 /** 478 * 479 * @return this 480 */ 481 public FeedbackTaskBuilder clearPhotos(){ 482 _details.setPhotoList(null); 483 return this; 484 } 485 486 /** 487 * 488 * @return this 489 */ 490 public FeedbackTaskBuilder clearReferencePhotos(){ 491 _details.setReferencePhotoList(null); 492 return this; 493 } 494 495 /** 496 * 497 * @return new task details based on the given data or null if no data was given 498 * @throws IllegalArgumentException on bad data 499 */ 500 public PhotoTaskDetails build() throws IllegalArgumentException { 501 boolean hasDeleted = !PhotoList.isEmpty(_details.getDeletedPhotoList()); 502 PhotoList photoList = _details.getPhotoList(); 503 boolean hasPhotos = !PhotoList.isEmpty(photoList); 504 SimilarPhotoList similarPhotoList = _details.getSimilarPhotoList(); 505 boolean hasSimilar = !PhotoList.isEmpty(similarPhotoList); 506 DissimilarPhotoList dissimilarPhotoList = _details.getDissimilarPhotoList(); 507 boolean hasDissimilar = !PhotoList.isEmpty(dissimilarPhotoList); 508 ReferencePhotoList referencePhotoList = _details.getReferencePhotoList(); 509 boolean hasReferences = !PhotoList.isEmpty(referencePhotoList); 510 511 // check for validity: 512 if(hasDeleted){ 513 if(hasPhotos || hasSimilar || hasDissimilar || hasReferences){ 514 throw new IllegalArgumentException("Deleted photos must appear alone."); 515 } // no need to validate the deleted photo list, it only requires guids 516 }else if(hasPhotos){ 517 if(hasSimilar || hasDissimilar || hasReferences){ 518 throw new IllegalArgumentException("Photos must appear alone."); 519 }else if(!PhotoList.isValid(photoList)){ 520 throw new IllegalArgumentException("Invalid photo list."); 521 } 522 }else if(hasReferences){ // this will accept both similar and dissimilar to be present, if they contain valid photos 523 if(!hasDissimilar && !hasSimilar){ 524 throw new IllegalArgumentException("References must have similar or dissimilar photos."); 525 } 526 527 if(hasDissimilar && !DissimilarPhotoList.isValid(dissimilarPhotoList)){ 528 throw new IllegalArgumentException("Invalid dissimilar photo list."); 529 } 530 531 if(hasSimilar && !SimilarPhotoList.isValid(similarPhotoList)){ 532 throw new IllegalArgumentException("Invalid similar photo list."); 533 } 534 535 if(!ReferencePhotoList.isValid(referencePhotoList)){ 536 throw new IllegalArgumentException("Invalid reference photo list."); 537 } 538 }else if(hasSimilar || hasDissimilar){ 539 throw new IllegalArgumentException("Similar and dissimilar photos cannot appear without references."); 540 }else{ 541 LOGGER.debug("No content."); 542 return null; 543 } 544 545 return _details; 546 } 547 } // class FeedbackTaskBuilder 548}