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.contentstorage; 017 018import java.util.ArrayList; 019import java.util.Collection; 020import java.util.Date; 021import java.util.EnumSet; 022import java.util.HashMap; 023import java.util.Iterator; 024import java.util.List; 025import java.util.Map; 026import java.util.Map.Entry; 027 028import org.apache.log4j.Logger; 029 030import service.tut.pori.contentanalysis.AccessDetails; 031import service.tut.pori.contentanalysis.AnalysisBackend.Capability; 032import service.tut.pori.contentanalysis.PhotoParameters; 033import service.tut.pori.contentanalysis.PhotoParameters.AnalysisType; 034import service.tut.pori.contentanalysis.AsyncTask.TaskType; 035import service.tut.pori.contentanalysis.CAContentCore; 036import service.tut.pori.contentanalysis.CAContentCore.ServiceType; 037import service.tut.pori.contentanalysis.PhotoFeedbackTask.FeedbackTaskBuilder; 038import service.tut.pori.contentanalysis.Photo; 039import service.tut.pori.contentanalysis.CAContentCore.Visibility; 040import service.tut.pori.contentanalysis.PhotoDAO; 041import service.tut.pori.contentanalysis.MediaObject.ConfirmationStatus; 042import service.tut.pori.contentanalysis.MediaObject.MediaObjectType; 043import service.tut.pori.contentanalysis.CAProperties; 044import service.tut.pori.contentanalysis.DeletedPhotoList; 045import service.tut.pori.contentanalysis.PhotoList; 046import service.tut.pori.contentanalysis.PhotoTaskDetails; 047import service.tut.pori.contentanalysis.MediaObject; 048import service.tut.pori.twitterjazz.TwitterExtractor; 049import service.tut.pori.twitterjazz.TwitterExtractor.ContentType; 050import service.tut.pori.twitterjazz.TwitterPhotoDescription; 051import service.tut.pori.twitterjazz.TwitterProfile; 052import service.tut.pori.twitterjazz.TwitterUserDetails; 053import core.tut.pori.context.ServiceInitializer; 054import core.tut.pori.users.UserIdentity; 055import core.tut.pori.utils.MediaUrlValidator.MediaType; 056import core.tut.pori.utils.UserIdentityLock; 057 058/** 059 * A storage handler, which can be used to retrieve data from Twitter for analysis. 060 * 061 * This class is only for photo content, use TwitterJazz if you require summarization support. 062 */ 063public final class TwitterPhotoStorage extends ContentStorage { 064 /** 065 * media object {@link service.tut.pori.contentanalysis.MediaObject.MediaObjectType#METADATA} name for twitter id 066 * @see service.tut.pori.twitterjazz.TwitterUserDetails#getTwitterId() 067 * @see service.tut.pori.contentanalysis.MediaObject 068 */ 069 public static final String METADATA_TWITTER_ID = "twitterId"; 070 /** 071 * media object {@link service.tut.pori.contentanalysis.MediaObject.MediaObjectType#METADATA} name for twitter screen name 072 * @see service.tut.pori.twitterjazz.TwitterUserDetails#getScreenName() 073 * @see service.tut.pori.contentanalysis.MediaObject 074 */ 075 public static final String METADATA_TWITTER_SCREEN_NAME = "twitterScreenName"; 076 /** Service type declaration for this storage */ 077 public static final ServiceType SERVICE_TYPE = ServiceType.TWITTER_PHOTO; 078 private static final PhotoParameters ANALYSIS_PARAMETERS; 079 static{ 080 ANALYSIS_PARAMETERS = new PhotoParameters(); 081 ANALYSIS_PARAMETERS.setAnalysisTypes(EnumSet.of(AnalysisType.FACE_DETECTION, AnalysisType.KEYWORD_EXTRACTION, AnalysisType.VISUAL)); 082 } 083 private static final EnumSet<Capability> CAPABILITIES = EnumSet.of(Capability.PHOTO_ANALYSIS); 084 private static final Logger LOGGER = Logger.getLogger(TwitterPhotoStorage.class); 085 private static final String PREFIX_MEDIA_OBJECT = "twitter_"; // prefix for created metadata objects 086 private static final UserIdentityLock USER_IDENTITY_LOCK = new UserIdentityLock(); 087 088 /** 089 * Create a Twitter photo storage with default autoschedule options. 090 */ 091 public TwitterPhotoStorage(){ 092 super(); 093 } 094 095 /** 096 * 097 * @param autoSchedule 098 */ 099 public TwitterPhotoStorage(boolean autoSchedule){ 100 super(autoSchedule); 101 } 102 103 @Override 104 public EnumSet<Capability> getBackendCapabilities() { 105 return CAPABILITIES; 106 } 107 108 /** 109 * 110 * @param extractor 111 * @return map of entries and photos or null if none available 112 */ 113 private Map<TwitterEntry, Photo> getTwitterPhotos(TwitterExtractor extractor){ 114 return getTwitterPhotos(extractor.getUserId(), null, extractor.getProfile(EnumSet.of(ContentType.PHOTO_DESCRIPTIONS))); 115 } 116 117 /** 118 * A helper method for converting TwitterPhotoDescriptions to Photo objects 119 * 120 * @param authenticatedUser 121 * @param map 122 * @param profile 123 * @return the passed map, new map if null was passed, or null if null or empty map was passed AND no new photos were extracted 124 */ 125 private Map<TwitterEntry, Photo> getTwitterPhotos(UserIdentity authenticatedUser, Map<TwitterEntry, Photo> map, TwitterProfile profile){ 126 if(profile == null){ 127 LOGGER.warn("Null profile."); 128 return map; 129 } 130 131 List<TwitterPhotoDescription> photoDescriptions = profile.getPhotoDescriptions(); 132 if(photoDescriptions == null){ 133 LOGGER.debug("No photos found."); 134 return map; 135 } 136 137 TwitterUserDetails user = profile.getUser(); 138 Visibility visibility = (user.isProtected() ? Visibility.PRIVATE : Visibility.PUBLIC); 139 String twitterId = user.getTwitterId(); 140 String screenName = user.getScreenName(); 141 142 if(map == null){ 143 map = new HashMap<>(photoDescriptions.size()); 144 } 145 for(Iterator<TwitterPhotoDescription> iter = photoDescriptions.iterator(); iter.hasNext();){ 146 TwitterPhotoDescription d = iter.next(); 147 Photo photo = new Photo(); 148 photo.setVisibility(visibility); 149 photo.setOwnerUserId(authenticatedUser); 150 photo.setServiceType(SERVICE_TYPE); 151 photo.setDescription(d.getDescription()); 152 Date updated = d.getCreatedTime(); 153 photo.setUpdated(updated); 154 String url = d.getEntityUrl(); 155 photo.setUrl(url); 156 String entityId = d.getEntityId(); 157 158 MediaObject object = new MediaObject(MediaType.PHOTO, MediaObjectType.METADATA); // add the origin of the photo as metadata 159 object.setVisibility(visibility); 160 object.setOwnerUserId(authenticatedUser); 161 object.setServiceType(SERVICE_TYPE); 162 object.setUpdated(updated); 163 object.setObjectId(PREFIX_MEDIA_OBJECT+entityId+"_"+twitterId); 164 object.setValue(twitterId); 165 object.setName(METADATA_TWITTER_ID); 166 object.setConfirmationStatus(ConfirmationStatus.USER_CONFIRMED); 167 object.setConfidence(Definitions.DEFAULT_CONFIDENCE); 168 object.setRank(Definitions.DEFAULT_RANK); 169 photo.addMediaObject(object); 170 171 object = new MediaObject(MediaType.PHOTO, MediaObjectType.METADATA); // add the origin of the photo as metadata 172 object.setVisibility(visibility); 173 object.setOwnerUserId(authenticatedUser); 174 object.setServiceType(SERVICE_TYPE); 175 object.setUpdated(updated); 176 object.setObjectId(PREFIX_MEDIA_OBJECT+entityId+"_"+screenName); 177 object.setValue(screenName); 178 object.setName(METADATA_TWITTER_SCREEN_NAME); 179 object.setConfirmationStatus(ConfirmationStatus.USER_CONFIRMED); 180 object.setConfidence(Definitions.DEFAULT_CONFIDENCE); 181 object.setRank(Definitions.DEFAULT_RANK); 182 photo.addMediaObject(object); 183 184 map.put(new TwitterEntry(entityId, url, null, screenName, authenticatedUser), photo); 185 } // for 186 187 return map; 188 } 189 190 /** 191 * 192 * @param extractor 193 * @param screenNames list of screen names 194 * @return map of entries and photos or null if none available 195 */ 196 private Map<TwitterEntry, Photo> getTwitterPhotos(TwitterExtractor extractor, Collection<String> screenNames){ 197 List<TwitterProfile> profiles = extractor.getProfiles(EnumSet.of(ContentType.PHOTO_DESCRIPTIONS), screenNames.toArray(new String[screenNames.size()])); 198 if(profiles == null){ 199 LOGGER.warn("No profiles found."); 200 return null; 201 } 202 203 Map<TwitterEntry, Photo> retval = null; 204 for(TwitterProfile profile : profiles){ 205 retval = getTwitterPhotos(extractor.getUserId(), retval, profile); 206 } 207 208 return retval; 209 } 210 211 @Override 212 public String getTargetUrl(AccessDetails details){ 213 return ServiceInitializer.getDAOHandler().getSQLDAO(TwitterDAO.class).getUrl(details.getGuid()); 214 } 215 216 @Override 217 public void removeMetadata(UserIdentity userId, Collection<String> guids){ 218 LOGGER.debug("Removing metadata for user, id: "+userId.getUserId()); 219 PhotoDAO photoDAO = ServiceInitializer.getDAOHandler().getSolrDAO(PhotoDAO.class); 220 PhotoList photos = photoDAO.getPhotos(null, guids, null, EnumSet.of(SERVICE_TYPE), new long[]{userId.getUserId()}); 221 if(PhotoList.isEmpty(photos)){ 222 LOGGER.debug("User, id: "+userId.getUserId()+" has no photos."); 223 return; 224 } 225 List<String> remove = photos.getGUIDs(); 226 photoDAO.remove(remove); 227 ServiceInitializer.getDAOHandler().getSQLDAO(TwitterDAO.class).removeEntries(remove); 228 229 FeedbackTaskBuilder builder = new FeedbackTaskBuilder(TaskType.FEEDBACK); // create builder for deleted photo feedback task 230 builder.setUser(userId); 231 builder.setBackends(getBackends()); 232 builder.addDeletedPhotos(DeletedPhotoList.getPhotoList(photos.getPhotos(), photos.getResultInfo())); 233 PhotoTaskDetails details = builder.build(); 234 if(details == null){ 235 LOGGER.warn("No content."); 236 }else{ 237 if(isAutoSchedule()){ 238 LOGGER.debug("Scheduling feedback task."); 239 CAContentCore.scheduleTask(details); 240 }else{ 241 LOGGER.debug("Auto-schedule is disabled."); 242 } 243 244 notifyFeedbackTaskCreated(details); 245 } 246 } 247 248 /** 249 * Note: the synchronization is only one-way, from Twitter to front-end, 250 * no information will be transmitted to the other direction. 251 * Also, tags removed from Twitter will NOT be removed from front-end. 252 * 253 * @param userId 254 * @return true on success 255 */ 256 @Override 257 public boolean synchronizeAccount(UserIdentity userId){ 258 return synchronizeAccount(userId, null); 259 } 260 261 /** 262 * Note: the synchronization is only one-way, from Twitter to front-end, 263 * no information will be transmitted to the other direction. 264 * Also, tags removed from Twitter will NOT be removed from front-end. 265 * 266 * Note that if screenNames are given, the synchronization is targeted ONLY to the given screen names. If screenNames are NOT given, the synchronization is targeted to ALL content. 267 * An example use case: 268 * <ul> 269 * <li>User synchronized his/her account with screenName <i>name1</i>, which in this case is also the name of the user's own Twitter account</li> 270 * <li>Then, the user re-syncs with different name, <i>name2</i>. The previously used content is left as it is, because the name <i>name1</i> is not given.</li> 271 * <li>Now, re-syncing with the name <i>name1</i> will ignore all content previously synced with <i>name2</i>, and only synchronize <i>name1</i>. Likewise, using <i>name2</i> would only sync <i>name2</i>, and ignore all content for <i>name1</i>.</li> 272 * <li>Re-syncing with both <i>name1</i> and <i>name2</i> would retrieve content for both accounts.</li> 273 * <li>Re-syncing without screenNames will default the retrieval to user's own account name (<i>name1</i>), but synchronize ALL content. In practice this means, that the content for <i>name1</i> will be synchronized, and all content for <i>name2</i> will be removed.</li> 274 * </ul> 275 * 276 * @param userId 277 * @param screenNames use the given collection of screen names instead of the authenticated user's account for synchronization 278 * @return true on success 279 */ 280 public boolean synchronizeAccount(UserIdentity userId, Collection<String> screenNames){ 281 USER_IDENTITY_LOCK.acquire(userId); 282 LOGGER.debug("Synchronizing account for user, id: "+userId.getUserId()); 283 try{ 284 TwitterExtractor extractor = TwitterExtractor.getExtractor(userId); 285 if(extractor == null){ 286 LOGGER.warn("Could not get extractor."); 287 return false; 288 } 289 290 Map<TwitterEntry, Photo> twitterPhotos = (screenNames == null || screenNames.isEmpty() ? getTwitterPhotos(extractor) : getTwitterPhotos(extractor, screenNames)); // in the end this will contain all the new items 291 TwitterDAO twitterDAO = ServiceInitializer.getDAOHandler().getSQLDAO(TwitterDAO.class); 292 293 List<TwitterEntry> existing = twitterDAO.getEntriesByScreenName(screenNames, userId); // in the end this will contain "lost" items: 294 PhotoDAO photoDao = ServiceInitializer.getDAOHandler().getSolrDAO(PhotoDAO.class); 295 if(twitterPhotos != null){ 296 if(existing != null){ 297 LOGGER.debug("Processing existing photos..."); 298 List<Photo> updatedPhotos = new ArrayList<>(); 299 for(Iterator<Entry<TwitterEntry, Photo>> entryIter = twitterPhotos.entrySet().iterator(); entryIter.hasNext();){ 300 Entry<TwitterEntry, Photo> entry = entryIter.next(); 301 TwitterEntry twitterEntry = entry.getKey(); 302 String entityId = twitterEntry.getEntityId(); 303 String screenName = twitterEntry.getScreenName(); 304 for(Iterator<TwitterEntry> existingIter = existing.iterator();existingIter.hasNext();){ 305 TwitterEntry exEntry = existingIter.next(); 306 if(exEntry.getEntityId().equals(entityId) && exEntry.getScreenName().equals(screenName)){ // already added 307 String guid = exEntry.getGUID(); 308 twitterEntry.setGUID(guid); 309 Photo p = entry.getValue(); 310 p.setGUID(guid); 311 updatedPhotos.add(p); // something may have changed 312 existingIter.remove(); // remove from existing to prevent deletion 313 entryIter.remove(); // remove from entries to prevent duplicate addition 314 break; 315 } 316 } // for siter 317 } // for 318 if(updatedPhotos.size() > 0){ 319 LOGGER.debug("Updating photo details..."); 320 photoDao.updatePhotosIfNewer(userId, PhotoList.getPhotoList(updatedPhotos, null)); 321 } 322 }else{ 323 LOGGER.debug("No existing photos."); 324 } 325 326 if(twitterPhotos.isEmpty()){ 327 LOGGER.debug("No new photos."); 328 }else{ 329 LOGGER.debug("Inserting photos..."); 330 if(!photoDao.insert(PhotoList.getPhotoList(twitterPhotos.values(), null))){ 331 LOGGER.error("Failed to add photos to database."); 332 return false; 333 } 334 335 for(Entry<TwitterEntry, Photo> e : twitterPhotos.entrySet()){ // update entries with correct GUIDs 336 e.getKey().setGUID(e.getValue().getGUID()); 337 } 338 339 LOGGER.debug("Creating photo entries..."); 340 twitterDAO.createEntries(twitterPhotos.keySet()); 341 } 342 }else{ 343 LOGGER.debug("No photos retrieved."); 344 } 345 346 int taskLimit = ServiceInitializer.getPropertyHandler().getSystemProperties(CAProperties.class).getMaxTaskSize(); 347 348 int missing = (existing == null ? 0 : existing.size()); 349 if(missing > 0){ // remove all "lost" items if any 350 LOGGER.debug("Deleting removed photos..."); 351 List<String> guids = new ArrayList<>(); 352 for(Iterator<TwitterEntry> iter = existing.iterator();iter.hasNext();){ 353 guids.add(iter.next().getGUID()); 354 } 355 356 photoDao.remove(guids); // remove photos 357 twitterDAO.removeEntries(guids); // remove entries 358 359 FeedbackTaskBuilder builder = new FeedbackTaskBuilder(TaskType.FEEDBACK); // create builder for deleted photo feedback task 360 builder.setUser(userId); 361 builder.setBackends(getBackends()); 362 363 if(taskLimit == CAProperties.MAX_TASK_SIZE_DISABLED || missing <= taskLimit){ // if task limit is disabled or there are less photos than the limit 364 builder.addDeletedPhotos(guids); 365 buildAndNotifyFeedback(builder); 366 }else{ // loop to stay below max limit 367 List<String> partial = new ArrayList<>(taskLimit); 368 int pCount = 0; 369 for(String guid : guids){ 370 if(pCount == taskLimit){ 371 builder.addDeletedPhotos(guids); 372 buildAndNotifyFeedback(builder); 373 pCount = 0; 374 partial.clear(); 375 builder.clearDeletedPhotos(); 376 } 377 ++pCount; 378 partial.add(guid); 379 } // for 380 if(!partial.isEmpty()){ 381 builder.addDeletedPhotos(guids); 382 buildAndNotifyFeedback(builder); 383 } 384 } // else 385 } 386 387 int twitterPhotoCount = (twitterPhotos == null ? 0 : twitterPhotos.size()); 388 LOGGER.debug("Added "+twitterPhotoCount+" photos, removed "+missing+" photos for user: "+userId.getUserId()); 389 390 if(twitterPhotoCount > 0){ 391 LOGGER.debug("Creating a new analysis task..."); 392 PhotoTaskDetails details = new PhotoTaskDetails(TaskType.ANALYSIS); 393 details.setUserId(userId); 394 details.setBackends(getBackends(getBackendCapabilities())); 395 details.setTaskParameters(ANALYSIS_PARAMETERS); 396 397 if(taskLimit == CAProperties.MAX_TASK_SIZE_DISABLED || twitterPhotoCount <= taskLimit){ // if task limit is disabled or there are less photos than the limit 398 details.setPhotoList(PhotoList.getPhotoList(twitterPhotos.values(), null)); 399 notifyAnalysis(details); 400 }else{ // loop to stay below max limit 401 List<Photo> partial = new ArrayList<>(taskLimit); 402 PhotoList partialContainer = new PhotoList(); 403 details.setPhotoList(partialContainer); 404 int pCount = 0; 405 for(Photo photo : twitterPhotos.values()){ 406 if(pCount == taskLimit){ 407 partialContainer.setPhotos(partial); 408 notifyAnalysis(details); 409 pCount = 0; 410 partial.clear(); 411 } 412 ++pCount; 413 partial.add(photo); 414 } // for 415 if(!partial.isEmpty()){ 416 partialContainer.setPhotos(partial); 417 notifyAnalysis(details); 418 } 419 } // else 420 }else{ 421 LOGGER.debug("No new photos, will not create analysis task."); 422 } 423 return true; 424 } finally { 425 USER_IDENTITY_LOCK.release(userId); 426 } 427 } 428 429 /** 430 * Helper method for calling notify 431 * 432 * @param details 433 */ 434 private void notifyAnalysis(PhotoTaskDetails details){ 435 details.setTaskId(null); //make sure the task id is not set for a new task 436 if(isAutoSchedule()){ 437 LOGGER.debug("Scheduling analysis task."); 438 CAContentCore.scheduleTask(details); 439 }else{ 440 LOGGER.debug("Auto-schedule is disabled."); 441 } 442 443 notifyAnalysisTaskCreated(details); 444 } 445 446 /** 447 * helper method for building the task and calling notify 448 * 449 * @param builder 450 */ 451 private void buildAndNotifyFeedback(FeedbackTaskBuilder builder){ 452 PhotoTaskDetails details = builder.build(); 453 if(details == null){ 454 LOGGER.warn("No content."); 455 }else{ 456 if(isAutoSchedule()){ 457 LOGGER.debug("Scheduling feedback task."); 458 CAContentCore.scheduleTask(details); 459 }else{ 460 LOGGER.debug("Auto-schedule is disabled."); 461 } 462 463 notifyFeedbackTaskCreated(details); 464 } 465 } 466 467 @Override 468 public ServiceType getServiceType() { 469 return SERVICE_TYPE; 470 } 471 472 /** 473 * A class that represents a single Twitter content entry. 474 */ 475 public static class TwitterEntry{ 476 private String _entityId = null; 477 private String _entityUrl = null; 478 private String _guid = null; 479 private String _screenName = null; 480 private UserIdentity _userId = null; 481 482 /** 483 * 484 * @param entityId 485 * @param entityUrl 486 * @param guid 487 * @param screenName 488 * @param userId 489 */ 490 public TwitterEntry(String entityId, String entityUrl, String guid, String screenName, UserIdentity userId){ 491 _entityId = entityId; 492 _entityUrl = entityUrl; 493 _guid = guid; 494 _screenName = screenName; 495 _userId = userId; 496 } 497 498 /** 499 * 500 */ 501 protected TwitterEntry(){ 502 // nothing needed 503 } 504 505 /** 506 * 507 * @param guid 508 */ 509 protected void setGUID(String guid) { 510 _guid = guid; 511 } 512 513 /** 514 * 515 * @return entity id 516 */ 517 public String getEntityId() { 518 return _entityId; 519 } 520 521 /** 522 * 523 * @return guid 524 */ 525 public String getGUID() { 526 return _guid; 527 } 528 529 /** 530 * @return the entityUrl 531 */ 532 public String getEntityUrl() { 533 return _entityUrl; 534 } 535 536 /** 537 * @return the userId 538 */ 539 public UserIdentity getUserId() { 540 return _userId; 541 } 542 543 /** 544 * @return the screenName 545 */ 546 public String getScreenName() { 547 return _screenName; 548 } 549 550 /** 551 * @param screenName the screenName to set 552 */ 553 protected void setScreenName(String screenName) { 554 _screenName = screenName; 555 } 556 557 /** 558 * 559 * @param userId 560 */ 561 protected void setUserId(UserIdentity userId) { 562 _userId = userId; 563 } 564 565 /** 566 * 567 * @param entityId 568 */ 569 protected void setEntityId(String entityId) { 570 _entityId = entityId; 571 } 572 573 /** 574 * 575 * @param entityUrl 576 */ 577 protected void setEntityUrl(String entityUrl) { 578 _entityUrl = entityUrl; 579 } 580 } // class TwitterEntry 581}