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}