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.ArrayList; 019import java.util.Date; 020import java.util.EnumSet; 021import java.util.HashMap; 022import java.util.Iterator; 023import java.util.List; 024import java.util.Map; 025 026import javax.xml.bind.annotation.XmlEnum; 027import javax.xml.bind.annotation.XmlEnumValue; 028 029import org.apache.commons.lang3.ArrayUtils; 030import org.apache.commons.lang3.StringUtils; 031import org.apache.log4j.Logger; 032import org.quartz.JobBuilder; 033import org.quartz.JobDataMap; 034import org.quartz.SchedulerException; 035import org.quartz.Trigger; 036import org.quartz.TriggerBuilder; 037 038import service.tut.pori.contentanalysis.AnalysisBackend.Capability; 039import service.tut.pori.contentanalysis.AsyncTask.TaskStatus; 040import service.tut.pori.contentanalysis.AsyncTask.TaskType; 041import service.tut.pori.contentanalysis.PhotoFeedbackTask.FeedbackTaskBuilder; 042import service.tut.pori.contentstorage.ContentStorageCore; 043import core.tut.pori.context.ServiceInitializer; 044import core.tut.pori.http.RedirectResponse; 045import core.tut.pori.http.parameters.DataGroups; 046import core.tut.pori.http.parameters.Limits; 047import core.tut.pori.users.UserIdentity; 048import core.tut.pori.utils.MediaUrlValidator.MediaType; 049 050/** 051 * 052 * This class includes functions for general content operations, such as updating the keywords, 053 * modifying photo details, and uploading new content 054 * 055 * Note: this does not have UserEventListener for user removal, this is because currently all content is managed by ContentStorage service, which will call all 056 * the necessary method for removing photo content (when needed) 057 */ 058public final class CAContentCore { 059 /** default capabilities for photo tasks */ 060 public static final EnumSet<Capability> DEFAULT_CAPABILITIES = EnumSet.of(Capability.USER_FEEDBACK, Capability.PHOTO_ANALYSIS, Capability.BACKEND_FEEDBACK); 061 private static final Logger LOGGER = Logger.getLogger(CAContentCore.class); 062 063 /** 064 * Service type declarations. 065 * 066 */ 067 @XmlEnum 068 public enum ServiceType { 069 /** content has been retrieved from Picasa, service id: {@value service.tut.pori.contentanalysis.Definitions#SERVICE_ID_PICASA} */ 070 @XmlEnumValue(value=Definitions.SERVICE_ID_PICASA) 071 PICASA_STORAGE_SERVICE(1), 072 /** content has been retrieved from FSIO, service id: {@value service.tut.pori.contentanalysis.Definitions#SERVICE_ID_FSIO} */ 073 @Deprecated 074 @XmlEnumValue(value=Definitions.SERVICE_ID_FSIO) 075 FSIO(2), 076 /** 077 * content has been retrieved from Facebook using the Facebook Jazz Service 078 * 079 * service id: {@value service.tut.pori.contentanalysis.Definitions#SERVICE_ID_FACEBOOK_JAZZ} 080 * 081 * @see service.tut.pori.facebookjazz.FBJContentCore 082 */ 083 @XmlEnumValue(value=Definitions.SERVICE_ID_FACEBOOK_JAZZ) 084 FACEBOOK_JAZZ(3), 085 /** content has been retrieved from Facebook, service id: {@value service.tut.pori.contentanalysis.Definitions#SERVICE_ID_FACEBOOK_PHOTO} */ 086 @XmlEnumValue(value=Definitions.SERVICE_ID_FACEBOOK_PHOTO) 087 FACEBOOK_PHOTO(4), 088 /** 089 * content has been retrieved from Twitter using the Twitter Jazz Service 090 * 091 * service id: {@value service.tut.pori.contentanalysis.Definitions#SERVICE_ID_TWITTER_JAZZ} 092 * 093 * @see service.tut.pori.twitterjazz.TJContentCore 094 */ 095 @XmlEnumValue(value=Definitions.SERVICE_ID_TWITTER_JAZZ) 096 TWITTER_JAZZ(5), 097 /** content has been retrieved from Twitter, service id: {@value service.tut.pori.contentanalysis.Definitions#SERVICE_ID_TWITTER_PHOTO} */ 098 @XmlEnumValue(value=Definitions.SERVICE_ID_TWITTER_PHOTO) 099 TWITTER_PHOTO(6), 100 /** 101 * content has been uploaded directly to the service using URLs 102 * 103 * service id: {@value service.tut.pori.contentanalysis.Definitions#SERVICE_ID_URL_STORAGE} 104 * 105 * @see service.tut.pori.contentstorage.ContentStorageCore#addUrls(UserIdentity, int[], List) 106 * */ 107 @XmlEnumValue(value=Definitions.SERVICE_ID_URL_STORAGE) 108 URL_STORAGE(7); 109 110 private int _id; 111 112 /** 113 * 114 * @param id 115 */ 116 private ServiceType(int id){ 117 _id = id; 118 } 119 120 /** 121 * 122 * @param id 123 * @return the service id converted to ServiceType 124 * @throws IllegalArgumentException on bad input 125 */ 126 public static ServiceType fromServiceId(Integer id) throws IllegalArgumentException{ 127 if(id != null){ 128 for(ServiceType e : ServiceType.values()){ 129 if(e._id == id) 130 return e; 131 } 132 } 133 throw new IllegalArgumentException("Bad "+ServiceType.class.toString()+" : "+id); 134 } 135 136 /** 137 * 138 * @param types 139 * @return true if the given set was null or empty 140 */ 141 public static boolean isEmpty(EnumSet<ServiceType> types){ 142 return (types == null || types.isEmpty() ? true : false); 143 } 144 145 /** 146 * 147 * @return service id of this type 148 */ 149 public int getServiceId(){ 150 return _id; 151 } 152 153 /** 154 * 155 * 156 * @param serviceIds 157 * @return set of service types or null (if empty array is passed OR the array contains only NONE) 158 * @throws IllegalArgumentException on bad input 159 */ 160 public static EnumSet<ServiceType> fromIdArray(int[] serviceIds) throws IllegalArgumentException{ 161 if(ArrayUtils.isEmpty(serviceIds)){ 162 return null; 163 } 164 EnumSet<ServiceType> set = EnumSet.noneOf(ServiceType.class); 165 for(int i=0;i<serviceIds.length;++i){ 166 set.add(fromServiceId(serviceIds[i])); 167 } 168 return set; 169 } 170 171 /** 172 * 173 * @param types 174 * @return the types as integer list or null if null or empty list was passed 175 */ 176 public static int[] toIdArray(EnumSet<ServiceType> types){ 177 if(ServiceType.isEmpty(types)){ 178 LOGGER.debug("No types."); 179 return null; 180 } 181 int[] array = new int[types.size()]; 182 int index = 0; 183 for(Iterator<ServiceType> iter = types.iterator(); iter.hasNext(); ++index){ 184 array[index] = iter.next().getServiceId(); 185 } 186 return array; 187 } 188 189 /** 190 * 191 * @param set 192 * @return the passed set as a id string (service_id=ID,ID,ID...) or null, if null, empty set, or set containing ServiceType.ALL is passed 193 */ 194 public static String toServiceIdString(EnumSet<ServiceType> set){ 195 if(set == null || set.size() < 1){ 196 return null; 197 }else{ 198 StringBuilder sb = new StringBuilder(Definitions.PARAMETER_SERVICE_ID+"="); 199 Iterator<ServiceType> iter = set.iterator(); 200 sb.append(iter.next().getServiceId()); 201 while(iter.hasNext()){ 202 sb.append(","); 203 sb.append(iter.next().getServiceId()); 204 } 205 return sb.toString(); 206 } 207 } 208 } // enum ServiceType 209 210 /** 211 * The visibility. 212 */ 213 @XmlEnum 214 public enum Visibility{ 215 /** content can be accessed by anyone */ 216 @XmlEnumValue(value=Definitions.VISIBILITY_PUBLIC) 217 PUBLIC(0), 218 /** content can be accessed only by the owner */ 219 @XmlEnumValue(value=Definitions.VISIBILITY_PRIVATE) 220 PRIVATE(1), 221 /** content can be accessed only by the users in the defined group */ 222 @XmlEnumValue(value=Definitions.VISIBILITY_GROUP) 223 GROUP(2); 224 225 private int _value; 226 227 /** 228 * 229 * @param value 230 */ 231 private Visibility(int value){ 232 _value = value; 233 } 234 235 /** 236 * 237 * @return the visibility as integer 238 */ 239 public final int toInt(){ 240 return _value; 241 } 242 243 /** 244 * 245 * @param value 246 * @return the value converted to Visibility 247 * @throws IllegalArgumentException on bad input 248 */ 249 public static Visibility fromInt(int value) throws IllegalArgumentException{ 250 for(Visibility v : Visibility.values()){ 251 if(v._value == value){ 252 return v; 253 } 254 } 255 throw new IllegalArgumentException("Bad "+Visibility.class.toString()+" : "+value); 256 } 257 } // enum Visibility 258 259 /** 260 * 261 */ 262 private CAContentCore() { 263 // nothing needed 264 } 265 266 /** 267 * 268 * @param response 269 * @throws IllegalArgumentException 270 */ 271 public static void taskFinished(PhotoTaskResponse response) throws IllegalArgumentException{ 272 validateTaskResponse(response); 273 274 LOGGER.debug("TaskId: "+response.getTaskId()+", backendId: "+response.getBackendId()); 275 276 switch(response.getTaskType()){ 277 case ANALYSIS: 278 PhotoAnalysisTask.taskFinished(response); 279 break; 280 case BACKEND_FEEDBACK: 281 LOGGER.debug("Using "+PhotoFeedbackTask.class.toString()+" for task of type "+TaskType.BACKEND_FEEDBACK); 282 case FEEDBACK: 283 PhotoFeedbackTask.taskFinished(response); 284 break; 285 case SEARCH: 286 LOGGER.debug("Received taskFinished to a search task: asynchronous search tasks are not supported."); 287 default: 288 throw new IllegalArgumentException("Unsupported "+Definitions.ELEMENT_TASK_TYPE); 289 } 290 } 291 292 /** 293 * 294 * @param response 295 * @throws IllegalArgumentException on null response, bad task id, bad backend id and/or bad task type 296 */ 297 public static void validateTaskResponse(TaskResponse response) throws IllegalArgumentException{ 298 if(response == null){ 299 throw new IllegalArgumentException("Failed to process response."); 300 } 301 Long taskId = response.getTaskId(); 302 if(taskId == null){ 303 throw new IllegalArgumentException("Invalid "+Definitions.ELEMENT_TASK_ID); 304 } 305 Integer backendId = response.getBackendId(); 306 if(backendId == null){ 307 throw new IllegalArgumentException("Invalid "+Definitions.ELEMENT_BACKEND_ID); 308 } 309 TaskType type = response.getTaskType(); 310 if(type == null){ 311 throw new IllegalArgumentException("Invalid "+Definitions.ELEMENT_TASK_TYPE); 312 } 313 } 314 315 /** 316 * 317 * @param guid 318 * @param type 319 * @return redirection URL for the given GUID and type or null if either one the given values was null 320 */ 321 public static String generateRedirectUrl(String guid, ServiceType type){ 322 if(type == null || StringUtils.isBlank(guid)){ 323 LOGGER.warn("GUID or service type was null."); 324 return null; 325 } 326 return ServiceInitializer.getPropertyHandler().getRESTBindContext()+Definitions.SERVICE_CA+"/"+Definitions.METHOD_REDIRECT+"?"+Definitions.PARAMETER_GUID+"="+guid+"&"+Definitions.PARAMETER_SERVICE_ID+"="+type.getServiceId(); 327 } 328 329 /** 330 * resolves dynamic /rest/r? redirection URL to static access URL 331 * 332 * @param authenticatedUser 333 * @param serviceType 334 * @param guid 335 * @return redirection to static URL referenced by the given parameters 336 */ 337 public static RedirectResponse generateTargetUrl(UserIdentity authenticatedUser, ServiceType serviceType, String guid){ 338 return ContentStorageCore.generateTargetUrl(authenticatedUser, serviceType, guid); 339 } 340 341 /** 342 * 343 * This method is called by back-ends to retrieve a list of photos to be analyzed. 344 * To query tasks status from back-end use queryTaskStatus. 345 * 346 * @param backendId 347 * @param taskId 348 * @param dataGroups 349 * @param limits 350 * @return the task or null if not found 351 */ 352 public static AbstractTaskDetails queryTaskDetails(Integer backendId, Long taskId, DataGroups dataGroups, Limits limits) { 353 return ServiceInitializer.getDAOHandler().getSQLDAO(PhotoTaskDAO.class).getTask(backendId, dataGroups, limits, taskId); 354 } 355 356 /** 357 * Note: if the details already contain a taskId, the task will NOT be re-added to the database, but simply re-scheduled. 358 * 359 * If the details contains no back-ends, default back-ends will be added. See {@link #DEFAULT_CAPABILITIES} 360 * 361 * @param details 362 * @return task id of the generated task, null if task could not be created 363 */ 364 public static Long scheduleTask(PhotoTaskDetails details) { 365 JobBuilder builder = getBuilder(details.getTaskType()); 366 Long taskId = details.getTaskId(); 367 if(taskId != null){ 368 LOGGER.debug("Task id already present for task, id: "+taskId); 369 }else{ 370 BackendStatusList backends = details.getBackends(); 371 if(BackendStatusList.isEmpty(backends)){ 372 LOGGER.debug("No back-ends given, using defaults..."); 373 List<AnalysisBackend> ends = ServiceInitializer.getDAOHandler().getSQLDAO(BackendDAO.class).getBackends(DEFAULT_CAPABILITIES); 374 if(ends == null){ 375 LOGGER.warn("Aborting task, no capable back-ends."); 376 return null; 377 } 378 379 backends = new BackendStatusList(); 380 backends.setBackendStatus(ends, TaskStatus.NOT_STARTED); 381 details.setBackends(backends); 382 } 383 384 taskId = ServiceInitializer.getDAOHandler().getSQLDAO(PhotoTaskDAO.class).insertTask(details); 385 if(taskId == null){ 386 LOGGER.error("Task schedule failed: failed to insert new photo task."); 387 return null; 388 } 389 } 390 391 if(scheduleTask(builder, taskId)){ 392 return taskId; 393 }else{ 394 LOGGER.error("Failed to schedule new task."); 395 return null; 396 } 397 } 398 399 /** 400 * 401 * @param builder 402 * @param taskId 403 * @return true if the task was successfully scheduled 404 * @throws IllegalArgumentException on bad values 405 * @see #schedule(JobBuilder) 406 */ 407 public static boolean scheduleTask(JobBuilder builder, Long taskId) throws IllegalArgumentException{ 408 if(taskId == null || builder == null){ 409 throw new IllegalArgumentException("Invalid task id or builder."); 410 } 411 JobDataMap data = new JobDataMap(); 412 AsyncTask.setTaskId(data, taskId); 413 builder.setJobData(data); 414 LOGGER.debug("Scheduling task, id: "+taskId); 415 return schedule(builder); 416 } 417 418 /** 419 * Uses the platform defined scheduler to schedule the given builder. 420 * This may add a scheduling delay depending on the system property configuration. 421 * 422 * Note that the task may not necessarily start <i>immediately</i>, but may be delayed because of other tasks already added into the queue. 423 * 424 * @param builder 425 * @return true if the job was successfully scheduled 426 * @throws IllegalArgumentException on bad parameters 427 */ 428 public static boolean schedule(JobBuilder builder) throws IllegalArgumentException { 429 if(builder == null){ 430 throw new IllegalArgumentException("No builder given."); 431 } 432 TriggerBuilder<Trigger> trigger = null; 433 long delay = ServiceInitializer.getPropertyHandler().getSystemProperties(CAProperties.class).getScheduleTaskDelay(); 434 if(delay == CAProperties.TASK_DELAY_DISABLED){ 435 LOGGER.debug("Scheduling new task to start NOW."); 436 trigger = TriggerBuilder.newTrigger().startNow(); 437 }else{ 438 LOGGER.debug("Scheduling new task to start in "+delay+" milliseconds."); 439 trigger = TriggerBuilder.newTrigger().startAt(new Date(System.currentTimeMillis()+delay)); 440 } 441 442 try { 443 ServiceInitializer.getExecutorHandler().getScheduler().scheduleJob(builder.build(), trigger.build()); 444 } catch (SchedulerException ex) { 445 LOGGER.error(ex, ex); 446 return false; 447 } 448 return true; 449 } 450 451 /** 452 * 453 * @param type 454 * @return new builder for the given type 455 * @throws UnsupportedOperationException on unsupported type 456 * @throws IllegalArgumentException on bad type 457 */ 458 private static JobBuilder getBuilder(TaskType type) throws UnsupportedOperationException, IllegalArgumentException{ 459 if(type == null){ 460 throw new IllegalArgumentException("Null type."); 461 } 462 switch (type) { 463 case ANALYSIS: 464 return JobBuilder.newJob(PhotoAnalysisTask.class); 465 case BACKEND_FEEDBACK: 466 return JobBuilder.newJob(PhotoBackendFeedbackTask.class); 467 case FEEDBACK: 468 return JobBuilder.newJob(PhotoFeedbackTask.class); 469 case SEARCH: 470 LOGGER.debug("Task schedule failed: asynchronous search tasks are not supported."); 471 default: 472 throw new UnsupportedOperationException("Unsupported TaskType: "+type.name()); 473 } 474 } 475 476 /** 477 * 478 * @param authenticatedUser 479 * @param guids 480 * @param dataGroups 481 * @param limits 482 * @param serviceTypes 483 * @param userIdFilters 484 * @return list of photos or null if none was found with the given parameters 485 */ 486 public static PhotoList getPhotos(UserIdentity authenticatedUser, List<String> guids, DataGroups dataGroups, Limits limits, EnumSet<ServiceType> serviceTypes, long[] userIdFilters){ 487 return ServiceInitializer.getDAOHandler().getSolrDAO(PhotoDAO.class).search(authenticatedUser, dataGroups, guids, limits, null, serviceTypes, userIdFilters); 488 } 489 490 /** 491 * This does NOT sync the changes back to content storage (e.g. picasa) to prevent conflicts in future synchronizations 492 * 493 * @param authenticatedUser 494 * @param photoList 495 * @throws IllegalArgumentException 496 */ 497 public static void updatePhotos(UserIdentity authenticatedUser, PhotoList photoList) throws IllegalArgumentException{ 498 if(!PhotoList.isValid(photoList)){ 499 throw new IllegalArgumentException("Received empty or invalid photoList."); 500 }else{ 501 FeedbackTaskBuilder builder = new FeedbackTaskBuilder(TaskType.FEEDBACK); 502 builder.setUser(authenticatedUser); 503 for(Photo photo : photoList.getPhotos()){ 504 if(MediaObjectList.isEmpty(photo.getMediaObjects())){ 505 LOGGER.debug("Ignored photo without media objects."); 506 }else{ 507 builder.addPhoto(photo); // add to feedback task 508 } 509 } 510 511 if(!ServiceInitializer.getDAOHandler().getSolrDAO(PhotoDAO.class).updatePhotos(authenticatedUser, photoList)){ 512 throw new IllegalArgumentException("Could not update photos."); 513 } 514 515 PhotoTaskDetails details = builder.build(); 516 if(details == null){ 517 LOGGER.debug("Nothing updated, will not generate feedback."); 518 }else{ 519 scheduleTask(details); 520 } 521 } 522 } 523 524 /** 525 * This will allow some amount of bad GUIDs to exist as long as there are enough to make a proper request: 526 * at least one ref photo must exist, at least one similar or dissimilar photo must exist. The GUIDs must be unique, 527 * the same GUID may not appear in similar, dissimilar and ref list. 528 * 529 * @param authenticatedUser must be given 530 * @param feedbackList 531 */ 532 public static void similarityFeedback(UserIdentity authenticatedUser, PhotoFeedbackList feedbackList){ 533 if(!UserIdentity.isValid(authenticatedUser)){ 534 throw new IllegalArgumentException("Invalid user."); 535 } 536 537 if(!PhotoFeedbackList.isValid(feedbackList)){ 538 throw new IllegalArgumentException("Invalid feedback."); 539 } 540 541 ReferencePhotoList referenceList = feedbackList.getReferencePhotos(); 542 List<String> guids = referenceList.getGUIDs(); 543 544 SimilarPhotoList simList = feedbackList.getSimilarPhotos(); 545 if(SimilarPhotoList.isEmpty(simList)){ 546 LOGGER.debug("No similar photos."); 547 simList = null; // make sure it is really null 548 }else{ 549 guids.addAll(simList.getGUIDs()); 550 } 551 552 DissimilarPhotoList disList = feedbackList.getDissimilarPhotos(); 553 if(DissimilarPhotoList.isEmpty(disList)){ 554 LOGGER.debug("No dissimilar photos."); 555 disList = null; // make sure it is really null 556 }else{ 557 guids.addAll(disList.getGUIDs()); 558 } 559 560 PhotoList found = ServiceInitializer.getDAOHandler().getSolrDAO(PhotoDAO.class).search(authenticatedUser, null, guids, null, null, null, null); // we use search as the user can give feedback to all photos he/she would get as search results 561 if(PhotoList.isEmpty(found)){ 562 LOGGER.warn("Ignored feedback for non-existing or unauthorized photos, for user, id: "+authenticatedUser.getUserId()); 563 return; 564 } 565 566 FeedbackTaskBuilder builder = new FeedbackTaskBuilder(TaskType.FEEDBACK); 567 for(Photo photo : referenceList.getPhotos()){ // validate the given photos through found to make sure no bad data has been given 568 String guid = photo.getGUID(); 569 Photo p = found.getPhoto(guid); 570 if(p == null){ 571 LOGGER.debug("Ignored reference photo with bad GUID: "+guid+" for user, id: "+authenticatedUser.getUserId()); 572 } 573 builder.addReferencePhoto(p); 574 } 575 576 if(simList != null){ 577 for(Photo photo : simList.getPhotos()){ // validate the given photos through found to make sure no bad data has been given 578 String guid = photo.getGUID(); 579 Photo p = found.getPhoto(guid); 580 if(p == null){ 581 LOGGER.debug("Ignored similar photo with bad GUID: "+guid+" for user, id: "+authenticatedUser.getUserId()); 582 } 583 builder.addSimilarPhoto(p); 584 } 585 } 586 587 if(disList != null){ 588 for(Photo photo : disList.getPhotos()){ // validate the given photos through found to make sure no bad data has been given 589 String guid = photo.getGUID(); 590 Photo p = found.getPhoto(guid); 591 if(p == null){ 592 LOGGER.debug("Ignored dissimilar photo with bad GUID: "+guid+" for user, id: "+authenticatedUser.getUserId()); 593 } 594 builder.addDissimilarPhoto(p); 595 } 596 } 597 598 builder.setUser(authenticatedUser); 599 PhotoTaskDetails details = builder.build(); 600 if(details == null){ 601 throw new IllegalArgumentException("Bad reference list."); 602 } 603 scheduleTask(details); 604 } 605 606 /** 607 * 608 * @param authenticatedUser 609 * @param dataGroups 610 * @param limits 611 * @param mediaTypes optional media types for the retrieval, if null or empty, all types will be searched for 612 * @param serviceTypes 613 * @param mediaObjectIdFilters 614 * @return list of media objects or null if none was found with the given parameters 615 */ 616 public static MediaObjectList getMediaObjects(UserIdentity authenticatedUser, DataGroups dataGroups, Limits limits, EnumSet<MediaType> mediaTypes, EnumSet<ServiceType> serviceTypes, List<String> mediaObjectIdFilters) { 617 MediaObjectList objects = null; 618 if(mediaObjectIdFilters != null){ // if there are ids, convert to objects to use as a filter 619 objects = new MediaObjectList(); 620 for(String mediaObjectId : mediaObjectIdFilters){ 621 MediaObject o = new MediaObject(); // the media object will have type of UNKNOWN 622 o.setMediaObjectId(mediaObjectId); 623 objects.addMediaObject(o); 624 } 625 } 626 if(mediaTypes == null || mediaTypes.isEmpty()){ 627 LOGGER.debug("Empty media type set given, using all..."); 628 mediaTypes = EnumSet.allOf(MediaType.class); 629 } 630 return ServiceInitializer.getDAOHandler().getSolrDAO(MediaObjectDAO.class).search(authenticatedUser, dataGroups, limits, mediaTypes, serviceTypes, null, null, objects); 631 } 632 633 /** 634 * Delete the given photos. Normal user (ROLE_USER) can only delete his/her own photos. Back-end user (ROLE_BACKEND) can delete any photos. 635 * 636 * @param authenticatedUser 637 * @param guids list of guids. Non-existent guids will be ignored. 638 * @return true on success, false on failure. Failure generally means a permission problem. 639 * @throws IllegalArgumentException on bad user id 640 */ 641 public static boolean deletePhotos(UserIdentity authenticatedUser, List<String> guids) throws IllegalArgumentException { 642 if(!UserIdentity.isValid(authenticatedUser)){ 643 throw new IllegalArgumentException("Invalid user."); 644 } 645 if(guids == null || guids.isEmpty()){ 646 LOGGER.warn("Ignored empty guid list."); 647 return true; 648 } 649 650 PhotoDAO dao = ServiceInitializer.getDAOHandler().getSolrDAO(PhotoDAO.class); 651 List<AccessDetails> details = dao.getAccessDetails(authenticatedUser, guids); 652 if(details == null){ 653 LOGGER.debug("None of the guids were found."); 654 return true; // the user has requested that the photos be deleted, and they are gone, so return success 655 } 656 657 guids = new ArrayList<>(guids.size()); // the found GUIDs 658 for(AccessDetails d : details){ // go through the list of found details and check the permissions 659 switch(d.getPermission()){ 660 case BACKEND_ACCESS: 661 LOGGER.debug("Granting delete permissions for user, id: "+authenticatedUser.getUserId()+" for photo, GUID: "+d.getGuid()+" using permissions: "+AccessDetails.Permission.BACKEND_ACCESS.name()); 662 case PRIVATE_ACCESS: 663 guids.add(d.getGuid()); 664 break; 665 default: 666 LOGGER.warn("Permission denied for user, id: "+authenticatedUser.getUserId()+" for photo, GUID: "+d.getGuid()); 667 return false; 668 } 669 } 670 671 ContentStorageCore.removeMetadata(authenticatedUser, guids, EnumSet.allOf(ServiceType.class)); // remove from content storage (if added) 672 dao.remove(guids); // remove from photo DAO 673 674 scheduleTask(new FeedbackTaskBuilder(TaskType.FEEDBACK) 675 .setUser(authenticatedUser) 676 .addDeletedPhotos(guids) 677 .build() 678 ); 679 680 return true; 681 } 682 683 /** 684 * Create and schedule the task for all capable back-ends included in the task designated by the task Id. The given back-end Id will not participate in the feedback task. 685 * 686 * @param backendId the back-end that send the task finished call, this back-end is automatically omitted from the list of target back-ends 687 * @param photos photos returned in task finished call 688 * @param taskId the id of the finished analysis task 689 */ 690 public static void scheduleBackendFeedback(Integer backendId, PhotoList photos, Long taskId){ 691 if(PhotoList.isEmpty(photos)){ 692 LOGGER.debug("Not scheduling back-end feedback: empty photo list."); 693 return; 694 } 695 696 BackendStatusList tBackends = ServiceInitializer.getDAOHandler().getSQLDAO(PhotoTaskDAO.class).getBackendStatus(taskId, null); 697 if(BackendStatusList.isEmpty(tBackends)){ 698 LOGGER.warn("No back-ends for the given task, or the task does not exist. Task id: "+taskId); 699 return; 700 } 701 702 List<AnalysisBackend> backends = ServiceInitializer.getDAOHandler().getSQLDAO(BackendDAO.class).getBackends(Capability.BACKEND_FEEDBACK); // get list of back-ends with compatible capabilities 703 if(backends == null){ 704 LOGGER.debug("No capable back-ends for back-end feedback."); 705 return; 706 } 707 708 BackendStatusList statuses = new BackendStatusList(); 709 for(AnalysisBackend backend : backends){ 710 Integer id = backend.getBackendId(); 711 if(id.equals(backendId)){ // ignore the given back-end id 712 LOGGER.debug("Ignoring the back-end id of task results, back-end id: "+backendId+", task, id: "+taskId); 713 }else if(tBackends.getBackendStatus(id) != null){ // and all back-ends not part of the task 714 statuses.setBackendStatus(new BackendStatus(backend, TaskStatus.NOT_STARTED)); 715 } 716 } 717 if(BackendStatusList.isEmpty(statuses)){ 718 LOGGER.debug("No capable back-ends for back-end feedback."); 719 return; 720 } 721 722 PhotoTaskDetails details = (new service.tut.pori.contentanalysis.PhotoBackendFeedbackTask.FeedbackTaskBuilder(TaskType.BACKEND_FEEDBACK)) 723 .setBackends(statuses) 724 .addPhotos(photos) 725 .build(); 726 Map<String, String> metadata = new HashMap<>(1); 727 metadata.put(Definitions.METADATA_RELATED_TASK_ID, taskId.toString()); 728 details.setMetadata(metadata); 729 730 scheduleTask(details); 731 } 732}