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.CAProperties; 033import service.tut.pori.contentanalysis.PhotoParameters; 034import service.tut.pori.contentanalysis.PhotoParameters.AnalysisType; 035import service.tut.pori.contentanalysis.AsyncTask.TaskType; 036import service.tut.pori.contentanalysis.CAContentCore; 037import service.tut.pori.contentanalysis.CAContentCore.ServiceType; 038import service.tut.pori.contentanalysis.PhotoFeedbackTask.FeedbackTaskBuilder; 039import service.tut.pori.contentanalysis.Photo; 040import service.tut.pori.contentanalysis.CAContentCore.Visibility; 041import service.tut.pori.contentanalysis.DeletedPhotoList; 042import service.tut.pori.contentanalysis.PhotoDAO; 043import service.tut.pori.contentanalysis.PhotoList; 044import service.tut.pori.contentanalysis.PhotoTaskDetails; 045import service.tut.pori.contentanalysis.MediaObject; 046import service.tut.pori.contentanalysis.MediaObject.ConfirmationStatus; 047import service.tut.pori.contentanalysis.MediaObject.MediaObjectType; 048import service.tut.pori.contentanalysis.MediaObjectList; 049import service.tut.pori.facebookjazz.FacebookExtractor; 050import service.tut.pori.facebookjazz.FacebookPhotoDescription; 051import service.tut.pori.facebookjazz.FacebookPhotoTag; 052import core.tut.pori.context.ServiceInitializer; 053import core.tut.pori.users.UserIdentity; 054import core.tut.pori.utils.MediaUrlValidator.MediaType; 055import core.tut.pori.utils.UserIdentityLock; 056 057 058/** 059 * A storage service for retrieving content from Facebook and creating photo analysis and feedback tasks based on the content. 060 * 061 * This class is only for photo content, use FacebookJazz if you require summarization support. 062 */ 063public final class FacebookPhotoStorage extends ContentStorage { 064 /** Service type declaration for this storage */ 065 public static final ServiceType SERVICE_TYPE = ServiceType.FACEBOOK_PHOTO; 066 private static final PhotoParameters ANALYSIS_PARAMETERS; 067 static{ 068 ANALYSIS_PARAMETERS = new PhotoParameters(); 069 ANALYSIS_PARAMETERS.setAnalysisTypes(EnumSet.of(AnalysisType.FACE_DETECTION, AnalysisType.KEYWORD_EXTRACTION, AnalysisType.VISUAL)); 070 } 071 private static final EnumSet<Capability> CAPABILITIES = EnumSet.of(Capability.PHOTO_ANALYSIS); 072 private static final Visibility DEFAULT_VISIBILITY = Visibility.PRIVATE; 073 private static final Logger LOGGER = Logger.getLogger(FacebookPhotoStorage.class); 074 private static final String PREFIX_VISUAL_OBJECT = "facebook_"; 075 private static final UserIdentityLock USER_IDENTITY_LOCK = new UserIdentityLock(); 076 077 /** 078 * 079 */ 080 public FacebookPhotoStorage(){ 081 super(); 082 } 083 084 /** 085 * 086 * @param autoSchedule 087 */ 088 public FacebookPhotoStorage(boolean autoSchedule){ 089 super(autoSchedule); 090 } 091 092 @Override 093 public String getTargetUrl(AccessDetails details){ 094 return ServiceInitializer.getDAOHandler().getSQLDAO(FacebookDAO.class).getUrl(details.getGuid()); 095 } 096 097 @Override 098 public EnumSet<Capability> getBackendCapabilities() { 099 return CAPABILITIES; 100 } 101 102 /** 103 * return map of user's photos, the user is taken from the passed extractor object 104 * 105 * @param extractor 106 * @return list of photos or null if the user has none 107 */ 108 private Map<FacebookEntry, Photo> getFacebookPhotos(FacebookExtractor extractor){ 109 List<FacebookPhotoDescription> photoDescriptions = extractor.getPhotoDescriptions(false, true); 110 if(photoDescriptions == null){ 111 LOGGER.debug("No photos found."); 112 return null; 113 } 114 115 Map<FacebookEntry, Photo> retval = new HashMap<>(photoDescriptions.size()); 116 UserIdentity userId = extractor.getUserId(); 117 118 for(Iterator<FacebookPhotoDescription> iter = photoDescriptions.iterator(); iter.hasNext();){ 119 FacebookPhotoDescription d = iter.next(); 120 Photo photo = new Photo(); 121 photo.setVisibility(DEFAULT_VISIBILITY); 122 photo.setOwnerUserId(userId); 123 photo.setServiceType(SERVICE_TYPE); 124 photo.setName(d.getDescription()); 125 Date updated = d.getUpdatedTime(); 126 photo.setUpdated(updated); 127 String url = d.getSource(); 128 photo.setUrl(url); 129 130 List<FacebookPhotoTag> tags = d.getTagList(); 131 String photoId = d.getId(); 132 if(tags != null){ 133 List<MediaObject> objects = new ArrayList<>(tags.size()); 134 for(FacebookPhotoTag t : tags){ 135 MediaObject o = new MediaObject(MediaType.PHOTO, MediaObjectType.KEYWORD); 136 o.setConfirmationStatus(ConfirmationStatus.USER_CONFIRMED); 137 String value = t.getName(); 138 o.setValue(value); 139 o.setOwnerUserId(userId); 140 o.setServiceType(t.getServiceType()); 141 o.setUpdated(updated); 142 o.setVisibility(DEFAULT_VISIBILITY); 143 o.setConfidence(Definitions.DEFAULT_CONFIDENCE); 144 o.setObjectId(PREFIX_VISUAL_OBJECT+photoId+"_"+value); 145 o.setRank(Definitions.DEFAULT_RANK); 146 objects.add(o); 147 } // for 148 photo.setMediaObjects(MediaObjectList.getMediaObjectList(objects, null)); 149 } // if 150 retval.put(new FacebookEntry(null, url, photoId, userId), photo); 151 } // for 152 153 return retval; 154 } 155 156 @Override 157 public void removeMetadata(UserIdentity userId, Collection<String> guids){ 158 LOGGER.debug("Removing metadata for user, id: "+userId.getUserId()); 159 PhotoDAO photoDAO = ServiceInitializer.getDAOHandler().getSolrDAO(PhotoDAO.class); 160 PhotoList photos = photoDAO.getPhotos(null, guids, null, EnumSet.of(SERVICE_TYPE), new long[]{userId.getUserId()}); 161 if(PhotoList.isEmpty(photos)){ 162 LOGGER.debug("User, id: "+userId.getUserId()+" has no photos."); 163 return; 164 } 165 List<String> remove = photos.getGUIDs(); 166 photoDAO.remove(remove); 167 ServiceInitializer.getDAOHandler().getSQLDAO(FacebookDAO.class).removeEntries(remove); 168 169 FeedbackTaskBuilder builder = new FeedbackTaskBuilder(TaskType.FEEDBACK); // create builder for deleted photo feedback task 170 builder.setUser(userId); 171 builder.setBackends(getBackends()); 172 builder.addDeletedPhotos(DeletedPhotoList.getPhotoList(photos.getPhotos(), photos.getResultInfo())); 173 PhotoTaskDetails details = builder.build(); 174 if(details == null){ 175 LOGGER.warn("No content."); 176 }else{ 177 if(isAutoSchedule()){ 178 LOGGER.debug("Scheduling feedback task."); 179 CAContentCore.scheduleTask(details); 180 }else{ 181 LOGGER.debug("Auto-schedule is disabled."); 182 } 183 184 notifyFeedbackTaskCreated(details); 185 } 186 } 187 188 /** 189 * Note: the synchronization is only one-way, from Facebook to front-end, 190 * no information will be transmitted to the other direction. 191 * Also, tags removed from Facebook will NOT be removed from front-end. 192 * 193 * @param userId 194 * @return true on success 195 */ 196 @Override 197 public boolean synchronizeAccount(UserIdentity userId){ 198 USER_IDENTITY_LOCK.acquire(userId); 199 LOGGER.debug("Synchronizing account for user, id: "+userId.getUserId()); 200 try{ 201 FacebookExtractor extractor = FacebookExtractor.getExtractor(userId); 202 if(extractor == null){ 203 LOGGER.warn("Could not resolve credentials."); 204 return false; 205 } 206 207 Map<FacebookEntry, Photo> facebookPhotos = getFacebookPhotos(extractor); // in the end this will contain all the new items 208 FacebookDAO facebookDAO = ServiceInitializer.getDAOHandler().getSQLDAO(FacebookDAO.class); 209 210 List<FacebookEntry> existing = facebookDAO.getEntries(userId); // in the end this will contain "lost" items: 211 PhotoDAO photoDao = ServiceInitializer.getDAOHandler().getSolrDAO(PhotoDAO.class); 212 if(facebookPhotos != null){ 213 if(existing != null){ 214 LOGGER.debug("Processing existing photos..."); 215 List<Photo> updatedPhotos = new ArrayList<>(); 216 for(Iterator<Entry<FacebookEntry, Photo>> entryIter = facebookPhotos.entrySet().iterator(); entryIter.hasNext();){ 217 Entry<FacebookEntry, Photo> entry = entryIter.next(); 218 FacebookEntry facebookEntry = entry.getKey(); 219 String objectId = facebookEntry.getObjectId(); 220 for(Iterator<FacebookEntry> existingIter = existing.iterator();existingIter.hasNext();){ 221 FacebookEntry exEntry = existingIter.next(); 222 if(exEntry.getObjectId().equals(objectId)){ // already added 223 String guid = exEntry.getGUID(); 224 facebookEntry.setGUID(guid); 225 Photo p = entry.getValue(); 226 p.setGUID(guid); 227 updatedPhotos.add(p); // something may have changed 228 existingIter.remove(); // remove from existing to prevent deletion 229 entryIter.remove(); // remove from entries to prevent duplicate addition 230 break; 231 } 232 } // for siter 233 } // for 234 if(updatedPhotos.size() > 0){ 235 LOGGER.debug("Updating photo details..."); 236 photoDao.updatePhotosIfNewer(userId, PhotoList.getPhotoList(updatedPhotos, null)); 237 } 238 }else{ 239 LOGGER.debug("No existing photos."); 240 } 241 242 if(facebookPhotos.isEmpty()){ 243 LOGGER.debug("No new photos."); 244 }else{ 245 LOGGER.debug("Inserting photos..."); 246 if(!photoDao.insert(PhotoList.getPhotoList(facebookPhotos.values(), null))){ 247 LOGGER.error("Failed to add photos to database."); 248 return false; 249 } 250 251 for(Entry<FacebookEntry, Photo> e : facebookPhotos.entrySet()){ // update entries with correct guids 252 e.getKey().setGUID(e.getValue().getGUID()); 253 } 254 255 LOGGER.debug("Creating photo entries..."); 256 facebookDAO.createEntries(facebookPhotos.keySet()); 257 } 258 }else{ 259 LOGGER.debug("No photos retrieved."); 260 } 261 int taskLimit = ServiceInitializer.getPropertyHandler().getSystemProperties(CAProperties.class).getMaxTaskSize(); 262 263 int missing = (existing == null ? 0 : existing.size()); 264 if(missing > 0){ // remove all "lost" items if any 265 LOGGER.debug("Deleting removed photos..."); 266 List<String> guids = new ArrayList<>(); 267 for(Iterator<FacebookEntry> iter = existing.iterator();iter.hasNext();){ 268 guids.add(iter.next().getGUID()); 269 } 270 271 photoDao.remove(guids); // remove photos 272 facebookDAO.removeEntries(guids); // remove entries 273 274 FeedbackTaskBuilder builder = new FeedbackTaskBuilder(TaskType.FEEDBACK); // create builder for deleted photo feedback task 275 builder.setUser(userId); 276 builder.setBackends(getBackends()); 277 278 if(taskLimit == CAProperties.MAX_TASK_SIZE_DISABLED || missing <= taskLimit){ // if task limit is disabled or there are less photos than the limit 279 builder.addDeletedPhotos(guids); 280 buildAndNotifyFeedback(builder); 281 }else{ // loop to stay below max limit 282 List<String> partial = new ArrayList<>(taskLimit); 283 int pCount = 0; 284 for(String guid : guids){ 285 if(pCount == taskLimit){ 286 builder.addDeletedPhotos(guids); 287 buildAndNotifyFeedback(builder); 288 pCount = 0; 289 partial.clear(); 290 builder.clearDeletedPhotos(); 291 } 292 ++pCount; 293 partial.add(guid); 294 } // for 295 if(!partial.isEmpty()){ 296 builder.addDeletedPhotos(guids); 297 buildAndNotifyFeedback(builder); 298 } 299 } // else 300 } 301 302 int facebookPhotoCount = (facebookPhotos == null ? 0 : facebookPhotos.size()); 303 LOGGER.debug("Added "+facebookPhotoCount+" photos, removed "+missing+" photos for user: "+userId.getUserId()); 304 305 if(facebookPhotoCount > 0){ 306 LOGGER.debug("Creating a new analysis task..."); 307 PhotoTaskDetails details = new PhotoTaskDetails(TaskType.ANALYSIS); 308 details.setUserId(userId); 309 details.setBackends(getBackends(getBackendCapabilities())); 310 details.setTaskParameters(ANALYSIS_PARAMETERS); 311 312 if(taskLimit == CAProperties.MAX_TASK_SIZE_DISABLED || facebookPhotoCount <= taskLimit){ // if task limit is disabled or there are less photos than the limit 313 details.setPhotoList(PhotoList.getPhotoList(facebookPhotos.values(), null)); 314 notifyAnalysis(details); 315 }else{ // loop to stay below max limit 316 List<Photo> partial = new ArrayList<>(taskLimit); 317 PhotoList partialContainer = new PhotoList(); 318 details.setPhotoList(partialContainer); 319 int pCount = 0; 320 for(Photo photo : facebookPhotos.values()){ 321 if(pCount == taskLimit){ 322 partialContainer.setPhotos(partial); 323 notifyAnalysis(details); 324 pCount = 0; 325 partial.clear(); 326 } 327 ++pCount; 328 partial.add(photo); 329 } // for 330 if(!partial.isEmpty()){ 331 partialContainer.setPhotos(partial); 332 notifyAnalysis(details); 333 } 334 } // else 335 }else{ 336 LOGGER.debug("No new photos, will not create analysis task."); 337 } 338 return true; 339 } finally { 340 USER_IDENTITY_LOCK.release(userId); 341 } 342 } 343 344 /** 345 * Helper method for calling notify 346 * 347 * @param details 348 */ 349 private void notifyAnalysis(PhotoTaskDetails details){ 350 details.setTaskId(null); //make sure the task id is not set for a new task 351 if(isAutoSchedule()){ 352 LOGGER.debug("Scheduling analysis task."); 353 CAContentCore.scheduleTask(details); 354 }else{ 355 LOGGER.debug("Auto-schedule is disabled."); 356 } 357 358 notifyAnalysisTaskCreated(details); 359 } 360 361 /** 362 * helper method for building the task and calling notify 363 * 364 * @param builder 365 */ 366 private void buildAndNotifyFeedback(FeedbackTaskBuilder builder){ 367 PhotoTaskDetails details = builder.build(); 368 if(details == null){ 369 LOGGER.warn("No content."); 370 }else{ 371 if(isAutoSchedule()){ 372 LOGGER.debug("Scheduling feedback task."); 373 CAContentCore.scheduleTask(details); 374 }else{ 375 LOGGER.debug("Auto-schedule is disabled."); 376 } 377 378 notifyFeedbackTaskCreated(details); 379 } 380 } 381 382 @Override 383 public ServiceType getServiceType() { 384 return SERVICE_TYPE; 385 } 386 387 /** 388 * Represents a single Facebook content entry. 389 * 390 */ 391 public static class FacebookEntry { 392 private String _guid = null; 393 private String _staticUrl = null; 394 private String _objectId = null; 395 private UserIdentity _userId = null; 396 397 /** 398 * 399 * @param guid 400 * @param staticUrl 401 * @param objectId 402 * @param userId 403 */ 404 public FacebookEntry(String guid, String staticUrl, String objectId, UserIdentity userId) { 405 _guid = guid; 406 _staticUrl = staticUrl; 407 _objectId = objectId; 408 _userId = userId; 409 } 410 411 /** 412 * 413 */ 414 protected FacebookEntry(){ 415 // nothing needed 416 } 417 418 /** 419 * @return the guid 420 */ 421 public String getGUID() { 422 return _guid; 423 } 424 425 /** 426 * @return the staticUrl 427 */ 428 public String getStaticUrl() { 429 return _staticUrl; 430 } 431 432 /** 433 * @return the objectId 434 */ 435 public String getObjectId() { 436 return _objectId; 437 } 438 439 /** 440 * @return the userId 441 */ 442 public UserIdentity getUserId() { 443 return _userId; 444 } 445 446 /** 447 * @param guid the guid to set 448 */ 449 protected void setGUID(String guid) { 450 _guid = guid; 451 } 452 453 /** 454 * @param staticUrl the staticUrl to set 455 */ 456 protected void setStaticUrl(String staticUrl) { 457 _staticUrl = staticUrl; 458 } 459 460 /** 461 * @param objectId the objectId to set 462 */ 463 protected void setObjectId(String objectId) { 464 _objectId = objectId; 465 } 466 467 /** 468 * @param userId the userId to set 469 */ 470 protected void setUserId(UserIdentity userId) { 471 _userId = userId; 472 } 473 } // class FacebookEntry 474}