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.facebookjazz;
017
018import java.util.ArrayList;
019import java.util.EnumSet;
020import java.util.Iterator;
021import java.util.List;
022
023import org.apache.commons.lang3.StringUtils;
024import org.apache.log4j.Logger;
025
026import service.tut.pori.contentanalysis.Definitions;
027import service.tut.pori.contentanalysis.Photo;
028import service.tut.pori.contentanalysis.PhotoDAO;
029import service.tut.pori.contentanalysis.PhotoList;
030import service.tut.pori.contentanalysis.MediaObject;
031import service.tut.pori.contentanalysis.MediaObjectList;
032import service.tut.pori.contentstorage.FacebookDAO;
033import service.tut.pori.contentstorage.FacebookPhotoStorage;
034import service.tut.pori.contentstorage.FacebookPhotoStorage.FacebookEntry;
035import service.tut.pori.facebookjazz.WeightModifier.WeightModifierType;
036import service.tut.pori.users.google.OAuth2Token;
037import service.tut.pori.users.facebook.FacebookUserDAO;
038
039import com.restfb.Connection;
040import com.restfb.DefaultFacebookClient;
041import com.restfb.Facebook;
042import com.restfb.Parameter;
043import com.restfb.Version;
044import com.restfb.types.CategorizedFacebookType;
045import com.restfb.types.Comment;
046import com.restfb.types.Event;
047import com.restfb.types.Group;
048import com.restfb.types.StatusMessage;
049import com.restfb.types.User;
050import com.restfb.types.Video;
051
052import core.tut.pori.context.ServiceInitializer;
053import core.tut.pori.http.parameters.DataGroups;
054import core.tut.pori.users.UserIdentity;
055
056
057/**
058 * A high-level client implementation for retrieving user's Facebook profile contents.
059 */
060public final class FacebookExtractor {
061  /** default maximum item limit for the extractor */
062  public static final int DEFAULT_LIMIT = 200;
063  private static final Logger LOGGER = Logger.getLogger(FacebookExtractor.class);
064
065  /* objects */
066  private static final String OBJECT_USER_DETAIL= "me";
067
068  /* connections */
069  private static final String CONNECTION_COMMENTS = "comments";
070  private static final String CONNECTION_EVENTS = "events";
071  private static final String CONNECTION_GROUPS = "groups";
072  private static final String CONNECTION_LIKES = "likes";
073  private static final String CONNECTION_PHOTOS = "photos";
074  private static final String CONNECTION_STATUSES = "statuses";
075  private static final String CONNECTION_USER_EVENTS = OBJECT_USER_DETAIL+"/"+CONNECTION_EVENTS;
076  private static final String CONNECTION_USER_GROUPS = OBJECT_USER_DETAIL+"/"+CONNECTION_GROUPS;
077  private static final String CONNECTION_USER_LIKES = OBJECT_USER_DETAIL+"/"+CONNECTION_LIKES;
078  private static final String CONNECTION_USER_PHOTOS = OBJECT_USER_DETAIL+"/"+CONNECTION_PHOTOS+"/uploaded";
079  private static final String CONNECTION_USER_STATUSES = OBJECT_USER_DETAIL+"/"+CONNECTION_STATUSES;
080
081  /* parameters */
082  private static final String PARAMETER_LIMIT = "limit";
083
084  /* FQL QUERIES */
085  private static final String FQL_SELECT_USER_DETAILS = "SELECT uid, name FROM user WHERE uid=me()";
086  private static final String FQL_SELECT_VIDEO_DETAILS = "SELECT owner, title, description, updated_time, created_time, vid FROM video WHERE owner=me()";
087  
088  private DefaultFacebookClient _client = null;
089  private UserIdentity _userId = null;  
090  private WeightModifierList _userTagWeights = null;
091  private WeightModifierList _defaultTagWeights = null;
092
093  /**
094   * 
095   * Valid content types for a profile
096   * 
097   */
098  public enum ContentType{
099    /** Facebook events */
100    EVENTS,
101    /** Include tags generated by other back-ends. Can only be used in combination with PHOTO_DESCRIPTION */
102    GENERATED_TAGS,
103    /** Facebook groups */
104    GROUPS,
105    /** Facebook likes */
106    LIKES,
107    /** Descriptions generated from Facebook photos/status messages */
108    PHOTO_DESCRIPTIONS,
109    /** Facebook status messages */
110    STATUS_MESSAGES,
111    /** Descriptions generated from Facebook videos/status messages */
112    VIDEO_DESCRIPTIONS;
113
114    /**
115     * 
116     * @param values
117     * @return values converted to content types
118     * @throws IllegalArgumentException
119     */
120    public static EnumSet<ContentType> fromString(List<String> values) throws IllegalArgumentException {
121      EnumSet<ContentType> contentTypes = null;
122      if(values != null && !values.isEmpty()){
123        contentTypes = EnumSet.noneOf(ContentType.class);
124        for(String value : values){
125          ContentType found = null;
126          for(ContentType t : values()){
127            if(t.name().equalsIgnoreCase(value)){
128              found = t;
129              break;
130            }
131          } // for types
132          if(found == null){
133            throw new IllegalArgumentException("Bad ContentType: "+value);
134          }
135          contentTypes.add(found);
136        } // for values
137      }
138      return contentTypes;
139    }
140  } // enum ContentType
141
142  /**
143   * 
144   * @param userId
145   * @return the extractor or null on failure
146   */
147  public static FacebookExtractor getExtractor(UserIdentity userId){
148    FacebookExtractor extractor = null;
149    OAuth2Token token = ServiceInitializer.getDAOHandler().getSQLDAO(FacebookUserDAO.class).getToken(userId);
150    if(token == null){
151      LOGGER.debug("No token.");
152      return null;
153    }
154    extractor = new FacebookExtractor(userId);
155    extractor._client = new DefaultFacebookClient(token.getAccessToken(), Version.UNVERSIONED);
156    return extractor;
157  }
158
159  /**
160   * 
161   * @param userId
162   * 
163   */
164  private FacebookExtractor(UserIdentity userId){
165    _userId = userId;
166  }
167
168  /**
169   * 
170   * @return user status messages, if any
171   * 
172   */
173  public List<FacebookStatusMessage> getStatusMessages(){
174    Connection<StatusMessage> statusConnection = _client.fetchConnection(CONNECTION_USER_STATUSES, StatusMessage.class, Parameter.with(PARAMETER_LIMIT, DEFAULT_LIMIT));
175    //also possible to use a timeframe: Parameter.with("since", new Date(1)),Parameter.with("until", new Date())
176    List<StatusMessage> messages = statusConnection.getData();
177    if(messages.isEmpty()){ // nothing received
178      return null;
179    }
180
181    List<FacebookStatusMessage> retval = new ArrayList<>();//restfb's lists do not support addAll
182    retval.addAll(FacebookStatusMessage.getFacebookStatusMessages(messages));
183
184    int received = messages.size();  // compare against the default limit to see if there are more messages
185    while(received == DEFAULT_LIMIT){   // connection.hasNext(), just like the FB's JSON next links cannot be trusted
186      statusConnection = _client.fetchConnectionPage(statusConnection.getNextPageUrl(), StatusMessage.class);
187      messages = statusConnection.getData();
188      received = messages.size();
189      if(received > 0){
190        retval.addAll(FacebookStatusMessage.getFacebookStatusMessages(messages));
191      }    
192    }
193
194    if(retval.isEmpty()){
195      return null;
196    }else{
197      Integer messageWeight = getWeight(WeightModifierType.STATUS_MESSAGE__MESSAGE);
198      Integer commentWeight = getWeight(WeightModifierType.STATUS_MESSAGE__COMMENT_MESSAGE);
199      if(messageWeight == null && commentWeight == null){
200        LOGGER.warn("No "+WeightModifierType.STATUS_MESSAGE__MESSAGE.name()+" or "+WeightModifierType.STATUS_MESSAGE__COMMENT_MESSAGE.name());
201        return retval;
202      }
203      for(FacebookStatusMessage message : retval){
204        message.setMessageWeight(messageWeight);
205        setCommentWeights(commentWeight, message.getMessageComments());
206      }
207      return retval;
208    }
209  }
210  
211  /**
212   * helper method for setting the comment weights
213   * @param list 
214   * @param commentWeight 
215   */
216  private void setCommentWeights(Integer commentWeight, List<FacebookComment> list){
217    if(list != null && !list.isEmpty()){
218      for(FacebookComment c : list){
219        c.setMessageWeight(commentWeight);
220      }
221    }
222  }
223  
224  /**
225   * 
226   * @param type
227   * @return weight value for the type
228   */
229  private Integer getWeight(WeightModifierType type){
230    if(_userTagWeights == null){
231      _userTagWeights = ServiceInitializer.getDAOHandler().getSQLDAO(FacebookJazzDAO.class).getWeightModifiers(_userId);
232      if(_userTagWeights == null){
233        LOGGER.debug("No user tag weights available.");
234        _userTagWeights = new WeightModifierList();
235      }
236    }
237    
238    Integer value = _userTagWeights.getModifier(type);
239    if(value == null){
240      if(_defaultTagWeights == null){
241        _defaultTagWeights = ServiceInitializer.getDAOHandler().getSQLDAO(FacebookJazzDAO.class).getWeightModifiers(null);
242      }
243      if(_defaultTagWeights == null){
244        LOGGER.debug("No default tag weights available.");
245        _defaultTagWeights = new WeightModifierList();
246        return null;
247      }
248      value = _defaultTagWeights.getModifier(type);
249    }
250    return value;
251  }
252
253  /**
254   * Note: this will only retrieve first {@value service.tut.pori.facebookjazz.FacebookExtractor#DEFAULT_LIMIT} videos
255   * 
256   * @return descriptions or null if none found
257   */
258  public List<FacebookVideoDescription> getVideoDescriptions(){
259    List<ExtractedVideo> videos = _client.executeFqlQuery(FQL_SELECT_VIDEO_DETAILS, ExtractedVideo.class, Parameter.with(PARAMETER_LIMIT, DEFAULT_LIMIT));
260
261    if(videos.isEmpty()){
262      return null;
263    }
264
265    List<User> users = _client.executeFqlQuery(FQL_SELECT_USER_DETAILS, User.class);
266    String userName = null;
267    if(!users.isEmpty()){
268      userName = users.get(0).getName();
269    }
270
271    StringBuilder likeBuilder = new StringBuilder("SELECT object_id FROM like WHERE object_id IN(");
272    List<FacebookVideoDescription> retval = new ArrayList<>();
273    for(Iterator<ExtractedVideo> iter = videos.iterator();iter.hasNext();){
274      FacebookVideoDescription desc = new FacebookVideoDescription(iter.next());
275      likeBuilder.append('\'');
276      likeBuilder.append(desc.getId());   // construct fql for like count retrieval
277      likeBuilder.append("',");
278      if(desc.isValid()){
279        desc.setFromName(userName);
280        retval.add(desc);
281      }
282    }
283
284    if(retval.isEmpty()){
285      return null;
286    }else{
287      Integer descriptionWeight = getWeight(WeightModifierType.VIDEO_DESCRIPTION__DESCRIPTION);
288      if(descriptionWeight == null){
289        LOGGER.warn("No "+WeightModifierType.VIDEO_DESCRIPTION__DESCRIPTION.name());
290      }
291      
292      likeBuilder.setCharAt(likeBuilder.length()-1, ')');
293      List<ObjectId> counts = new ArrayList<>(_client.executeFqlQuery(likeBuilder.toString(), ObjectId.class)); // create new to allow modifications to the list
294      for(Iterator<FacebookVideoDescription> vdIter = retval.iterator();vdIter.hasNext();){
295        FacebookVideoDescription d = vdIter.next();
296        d.setDescriptionWeight(descriptionWeight); // set weight
297        retrieveComments(d); // retrieve comments
298        String objectId = d.getId();
299        long count = 0;
300        // go through the count list, make sure every description gets a like count value
301        for(Iterator<ObjectId> vIter = counts.iterator();vIter.hasNext();){
302          ObjectId c = vIter.next();
303          if(c.getObjectId().equals(objectId)){
304            ++count;
305            vIter.remove();
306          }
307        }
308        d.setLikeCount(count);
309      }  // for descriptions
310      
311      return retval;
312    }
313  }
314
315  /**
316   * 
317   * @param desc the list of comments will be set to the desc if any are found
318   */
319  private void retrieveComments(FacebookVideoDescription desc){
320    String objectId = desc.getId();
321    if(StringUtils.isBlank(objectId)){
322      LOGGER.warn("Could not retrieve comments, objectId was missing.");
323      return;
324    }
325    Connection<Comment> commentConnection = _client.fetchConnection(objectId+'/'+CONNECTION_COMMENTS, Comment.class, Parameter.with(PARAMETER_LIMIT, DEFAULT_LIMIT));
326    //also possible to use a timeframe: Parameter.with("since", new Date(1)),Parameter.with("until", new Date())
327    List<Comment> comments = commentConnection.getData();
328    if(comments.isEmpty()){ // nothing received
329      LOGGER.debug("No comments for video: "+objectId);
330      return;
331    }
332    List<FacebookComment> retval = new ArrayList<>();//restfb's lists do not support addAll
333    retval.addAll(FacebookComment.getCommentList(comments));
334
335    int received = comments.size();  // compare against the default limit to see if there are more messages
336    while(received == DEFAULT_LIMIT){   // connection.hasNext(), just like the FB's JSON next links cannot be trusted
337      commentConnection = _client.fetchConnectionPage(commentConnection.getNextPageUrl(), Comment.class);
338      comments = commentConnection.getData();
339      received = comments.size();
340      if(received > 0){
341        retval.addAll(FacebookComment.getCommentList(comments));
342      }    
343    }
344    desc.setDescriptionComments(retval);
345    
346    Integer commentWeight = getWeight(WeightModifierType.VIDEO_DESCRIPTION__COMMENT_MESSAGE);
347    if(commentWeight == null){
348      LOGGER.warn("No "+WeightModifierType.VIDEO_DESCRIPTION__COMMENT_MESSAGE.name());
349    }else{
350      setCommentWeights(commentWeight, retval); // set comment weights
351    }
352  }
353
354  /**
355   * 
356   * @return likes or null if none found
357   */
358  public List<FacebookLike> getLikes(){
359    Connection<CategorizedFacebookType> likeConnection = _client.fetchConnection(CONNECTION_USER_LIKES, CategorizedFacebookType.class, Parameter.with(PARAMETER_LIMIT, DEFAULT_LIMIT));
360
361    List<CategorizedFacebookType> likes = likeConnection.getData();
362    if(likes.isEmpty()){ // nothing received
363      return null;
364    }
365
366    List<FacebookLike> retval = new ArrayList<>();//restfb's lists do not support addAll
367    retval.addAll(FacebookLike.getFacebookLikes(likes));
368
369    int received = likes.size();  // compare against the default limit to see if there are more messages
370    while(received == DEFAULT_LIMIT){   // connection.hasNext(), just like the FB's JSON next links cannot be trusted
371      likeConnection = _client.fetchConnectionPage(likeConnection.getNextPageUrl(), CategorizedFacebookType.class);
372      likes = likeConnection.getData();
373      received = likes.size();
374      if(received > 0){
375        retval.addAll(FacebookLike.getFacebookLikes(likes));
376      }    
377    }
378
379    if(retval.isEmpty()){
380      return null;
381    }else{
382      return retval;
383    }
384  }
385
386  /**
387   * 
388   * @return groups or null if none found
389   */
390  public List<FacebookGroup> getGroups(){
391    Connection<Group> groupConnection = _client.fetchConnection(CONNECTION_USER_GROUPS, Group.class, Parameter.with(PARAMETER_LIMIT, DEFAULT_LIMIT));
392
393    List<Group> groups = groupConnection.getData();
394    if(groups.isEmpty()){ // nothing received
395      return null;
396    }
397
398    List<FacebookGroup> retval = new ArrayList<>();//restfb's lists do not support addAll
399    retval.addAll(FacebookGroup.getFacebookGroups(groups));
400
401    int received = groups.size();  // compare against the default limit to see if there are more messages
402    while(received == DEFAULT_LIMIT){   // connection.hasNext(), just like the FB's JSON next links cannot be trusted
403      groupConnection = _client.fetchConnectionPage(groupConnection.getNextPageUrl(), Group.class);
404      groups = groupConnection.getData();
405      received = groups.size();
406      if(received > 0){
407        retval.addAll(FacebookGroup.getFacebookGroups(groups));
408      }    
409    }
410    
411    if(retval.isEmpty()){
412      return null;
413    }else{
414      Integer nameWeight = getWeight(WeightModifierType.GROUP__NAME);
415      Integer descriptionWeight = getWeight(WeightModifierType.GROUP__DESCRIPTION);
416      if(nameWeight == null && descriptionWeight == null){
417        LOGGER.warn("No "+WeightModifierType.GROUP__NAME.name()+" or "+WeightModifierType.GROUP__DESCRIPTION.name());
418        return retval;
419      }
420      
421      for(FacebookGroup g : retval){
422        g.setDescriptionWeight(descriptionWeight);
423        g.setNameWeight(nameWeight);
424      }
425      
426      return retval;
427    }
428  }
429
430  /**
431   * 
432   * @return events or null if none was found
433   */
434  public List<FacebookEvent> getEvents(){
435    Connection<Event> eventConnection = _client.fetchConnection(CONNECTION_USER_EVENTS, Event.class, Parameter.with(PARAMETER_LIMIT, DEFAULT_LIMIT));
436
437    List<Event> events = eventConnection.getData();
438    if(events.isEmpty()){ // nothing received
439      return null;
440    }
441
442    List<FacebookEvent> retval = new ArrayList<>();//restfb's lists do not support addAll
443    retval.addAll(FacebookEvent.getFacebookEvents(events));
444
445    int received = events.size();  // compare against the default limit to see if there are more messages
446    while(received == DEFAULT_LIMIT){   // connection.hasNext(), just like the FB's JSON next links cannot be trusted
447      eventConnection = _client.fetchConnectionPage(eventConnection.getNextPageUrl(), Event.class);
448      events = eventConnection.getData();
449      received = events.size();
450      if(received > 0){
451        retval.addAll(FacebookEvent.getFacebookEvents(events));
452      }
453    }
454
455    if(retval.isEmpty()){
456      return null;
457    }else{
458      Integer descriptionWeight = getWeight(WeightModifierType.EVENT__DESCRIPTION);
459      Integer nameWeight = getWeight(WeightModifierType.EVENT__NAME);
460      if(nameWeight == null && descriptionWeight == null){
461        LOGGER.warn("No "+WeightModifierType.EVENT__DESCRIPTION.name()+" or "+WeightModifierType.EVENT__NAME.name());
462        return retval;
463      }
464      
465      for(FacebookEvent e : retval){
466        e.setDescriptionWeight(descriptionWeight);
467        e.setNameWeight(nameWeight);
468      }
469      
470      return retval;
471    }
472  }
473  
474  /**
475   * This method will not return generated tags
476   * 
477   * @return list of photo descriptions, ignoring photos without descriptions or comments
478   */
479  public List<FacebookPhotoDescription> getPhotoDescriptions(){
480    return getPhotoDescriptions(false, false);
481  }
482
483  /**
484   * 
485   * @param generatedTags if true the previously generated tags will be retrieved from the database and will be included in the results
486   * @param includeEmpty
487   * @return descriptions or null if none was found
488   */
489  public List<FacebookPhotoDescription> getPhotoDescriptions(boolean generatedTags, boolean includeEmpty){
490    Connection<com.restfb.types.Photo> photoConnection = _client.fetchConnection(CONNECTION_USER_PHOTOS, com.restfb.types.Photo.class, Parameter.with(PARAMETER_LIMIT, DEFAULT_LIMIT));
491    List<com.restfb.types.Photo> photos = photoConnection.getData();
492    if(photos.isEmpty()){ // nothing received
493      return null;
494    }
495
496    List<FacebookPhotoDescription> retval = new ArrayList<>();//for addAll
497    for(Iterator<com.restfb.types.Photo> iter = photos.iterator();iter.hasNext();){
498      FacebookPhotoDescription photo = new FacebookPhotoDescription(iter.next());
499      if(includeEmpty || photo.isValid()){
500        retval.add(photo);
501      }
502    }
503
504    int received = photos.size();  // compare against the default limit to see if there are more messages
505    while(received == DEFAULT_LIMIT){   // connection.hasNext(), just like the FB's JSON next links cannot be trusted
506      photoConnection = _client.fetchConnectionPage(photoConnection.getNextPageUrl(),com.restfb.types.Photo.class);
507      photos = photoConnection.getData();
508
509      for(Iterator<com.restfb.types.Photo> iter = photos.iterator();iter.hasNext();){
510        FacebookPhotoDescription photo = new FacebookPhotoDescription(iter.next());
511        if(includeEmpty || photo.isValid()){
512          retval.add(photo);
513        } // if
514      } // for
515    } // while
516
517    if(retval.isEmpty()){
518      return null;
519    }else{
520      Integer descriptionWeight = getWeight(WeightModifierType.PHOTO_DESCRIPTION__DESCRIPTION);
521      Integer commentWeight = getWeight(WeightModifierType.PHOTO_DESCRIPTION__COMMENT_MESSAGE);
522      if(commentWeight == null && descriptionWeight == null){
523        LOGGER.warn("No "+WeightModifierType.PHOTO_DESCRIPTION__DESCRIPTION.name()+" or "+WeightModifierType.PHOTO_DESCRIPTION__COMMENT_MESSAGE.name());
524        return retval;
525      }
526      
527      List<String> objectIds = new ArrayList<>(retval.size());
528      for(FacebookPhotoDescription d : retval){
529        d.setDescriptionWeight(descriptionWeight);
530        setCommentWeights(commentWeight, d.getDescriptionComments());
531        objectIds.add(d.getId());
532      }
533      
534      List<FacebookEntry> entries = ServiceInitializer.getDAOHandler().getSQLDAO(FacebookDAO.class).getEntries(objectIds, _userId);
535      if(entries == null){
536        LOGGER.debug("None of the photos are known by the system.");
537      }else{
538        PhotoList gTags = null;
539        if(generatedTags){
540          LOGGER.debug("Retrieving generated tags.");
541          List<String> guids = new ArrayList<>(entries.size());
542          for(FacebookEntry e : entries){
543            guids.add(e.getGUID());
544          }
545          gTags = ServiceInitializer.getDAOHandler().getSolrDAO(PhotoDAO.class).getPhotos(new DataGroups(Definitions.DATA_GROUP_KEYWORDS), guids, null, null, null);
546        }
547        
548        LOGGER.debug("Resolving photo GUIDs for descriptions.");
549        for(FacebookPhotoDescription d : retval){
550          String objectId = d.getId();
551          for(Iterator<FacebookEntry> eIter = entries.iterator(); eIter.hasNext();){
552            FacebookEntry e = eIter.next();
553            if(e.getObjectId().equals(objectId)){ // if the new description was found in the list of already known entries
554              String guid = e.getGUID();
555              d.setPhotoGUID(guid);
556              d.setServiceType(FacebookPhotoStorage.SERVICE_TYPE); // no need to check from database, all photos from TwitterDAO entries are of the same type
557              if(gTags != null){  // if tags were found
558                Photo p = gTags.getPhoto(guid); // in practice this should always return a photo...
559                if(p != null){
560                  MediaObjectList objects = p.getMediaObjects();
561                  if(!MediaObjectList.isEmpty(objects)){
562                    for(MediaObject vo : objects.getMediaObjects()){
563                      d.addTag(FacebookPhotoTag.getFacebookTag(vo));
564                    } // for
565                  } // if photo had media objects
566                }else{ // ..though there is theoretical possibility that the photo has been removed in between retrievals (which are not in a transaction), and were not found anymore
567                  LOGGER.warn("No photo found, GUID: "+guid);
568                  d.setPhotoGUID(null); // not valid anymore
569                } // else
570              } // if
571              eIter.remove();
572              break;
573            } // if
574          } // for entries
575        } // for descriptions
576      }
577      
578      return retval;
579    }
580  }
581
582  /**
583   * @return the userId
584   */
585  public UserIdentity getUserId() {
586    return _userId;
587  }
588
589  /**
590   * 
591   * @param contentTypes list of content types, by default only the basic user details are returned
592   * @return the extracted profile
593   * @throws IllegalArgumentException on incompatible content types
594   */
595  public FacebookProfile getProfile(EnumSet<ContentType> contentTypes) throws IllegalArgumentException{
596    FacebookUserDetails user = new FacebookUserDetails(_client.fetchObject(OBJECT_USER_DETAIL, User.class));
597    user.setUserId(_userId);
598    FacebookProfile profile = new FacebookProfile(user);
599
600    if(contentTypes != null && !contentTypes.isEmpty()){
601      boolean generatedTags = contentTypes.contains(ContentType.GENERATED_TAGS);
602      if(generatedTags && contentTypes.size() == 1){
603        throw new IllegalArgumentException("Only "+ContentType.GENERATED_TAGS.name()+" given.");
604      }
605      
606      if(contentTypes.contains(ContentType.STATUS_MESSAGES)){
607        profile.setStatusMessages(getStatusMessages());
608      }       
609      if(contentTypes.contains(ContentType.LIKES)){
610        profile.setLikes(getLikes());
611      }
612      if(contentTypes.contains(ContentType.EVENTS)){
613        profile.setEvents(getEvents());
614      }
615      if(contentTypes.contains(ContentType.GROUPS)){
616        profile.setGroups(getGroups());
617      }
618      if(contentTypes.contains(ContentType.VIDEO_DESCRIPTIONS)){
619        profile.setVideoDescriptions(getVideoDescriptions());
620      }
621      if(contentTypes.contains(ContentType.PHOTO_DESCRIPTIONS)){
622        profile.setPhotoDescriptions(getPhotoDescriptions(generatedTags, false));  
623      }
624    }else{
625      LOGGER.debug("No content types requested.");
626    }
627    return profile;
628  }
629
630  /**
631   * 
632   * Helper class for extracting a list of object ids
633   * 
634   */
635  private static class ObjectId{
636    @Facebook(value = "object_id")
637    private String _objectId = null;
638
639    /**
640     * 
641     * @return object id value
642     */
643    public String getObjectId(){
644      return _objectId;
645    }
646  }
647  
648  /**
649   * Extended to contain video id from FQL queries
650   *
651   */
652  public static class ExtractedVideo extends Video{
653    /** serial version UID */
654    private static final long serialVersionUID = 4183565698345218940L;
655    @Facebook(value="vid")
656    private String _objectId = null;
657    
658    /**
659     * @return object id (vid) or id if no object id is given
660     */
661    @Override
662    public String getId() {
663      return (StringUtils.isBlank(_objectId) ? super.getId() : _objectId);
664    }
665    
666  } // class ExtractedVideo
667}