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.twitterjazz;
017
018import java.util.Collection;
019import java.util.EnumSet;
020import java.util.List;
021import java.util.Set;
022
023import org.apache.log4j.Logger;
024import org.quartz.Job;
025import org.quartz.JobBuilder;
026import org.quartz.JobDataMap;
027import org.quartz.JobExecutionContext;
028import org.quartz.JobExecutionException;
029import org.springframework.context.ApplicationListener;
030
031import service.tut.pori.contentanalysis.AnalysisBackend;
032import service.tut.pori.contentanalysis.AnalysisBackend.Capability;
033import service.tut.pori.contentanalysis.AsyncTask.TaskStatus;
034import service.tut.pori.contentanalysis.BackendDAO;
035import service.tut.pori.contentanalysis.BackendStatusList;
036import service.tut.pori.contentanalysis.CAContentCore;
037import service.tut.pori.contentanalysis.CAContentCore.ServiceType;
038import service.tut.pori.contentanalysis.MediaObject;
039import service.tut.pori.contentanalysis.MediaObjectDAO;
040import service.tut.pori.contentanalysis.MediaObjectList;
041import service.tut.pori.contentanalysis.PhotoDAO;
042import service.tut.pori.twitterjazz.TwitterExtractor.ContentType;
043import service.tut.pori.users.twitter.TwitterProperties;
044import service.tut.pori.users.twitter.TwitterUserCore;
045import twitter4j.TwitterFactory;
046import twitter4j.conf.Configuration;
047import twitter4j.conf.ConfigurationBuilder;
048import core.tut.pori.context.ServiceInitializer;
049import core.tut.pori.http.parameters.DataGroups;
050import core.tut.pori.http.parameters.Limits;
051import core.tut.pori.http.parameters.SortOptions;
052import core.tut.pori.users.UserEvent;
053import core.tut.pori.users.UserEvent.EventType;
054import core.tut.pori.users.UserIdentity;
055import core.tut.pori.utils.MediaUrlValidator.MediaType;
056
057/**
058 * TwitterJazz core methods.
059 * 
060 */
061public final class TJContentCore {
062  /** default capabilities for Twitter tasks */
063  public static final EnumSet<Capability> DEFAULT_CAPABILITIES = EnumSet.of(Capability.TWITTER_SUMMARIZATION, Capability.PHOTO_ANALYSIS, Capability.BACKEND_FEEDBACK);
064  private static final Logger LOGGER = Logger.getLogger(TJContentCore.class);
065  private static final String JOB_KEY_USER_ID = "userId";
066  private static final EnumSet<MediaType> MEDIA_TYPES_TJ = EnumSet.allOf(MediaType.class);
067  private static final EnumSet<ServiceType> SERVICE_TYPES_TJ = EnumSet.of(ServiceType.TWITTER_JAZZ);
068  private static TwitterFactory TWITTER_FACTORY = null;
069  
070  /**
071   * 
072   */
073  private TJContentCore(){
074    // nothing needed
075  }
076  
077  /**
078   * 
079   * @param response
080   * @throws IllegalArgumentException
081   */
082  public static void taskFinished(TwitterTaskResponse response) throws IllegalArgumentException {
083    CAContentCore.validateTaskResponse(response);
084
085    LOGGER.debug("TaskId: "+response.getTaskId()+", backendId: "+response.getBackendId());
086    
087    switch(response.getTaskType()){
088      case TWITTER_PROFILE_SUMMARIZATION:
089        TwitterSummarizationTask.taskFinished(response);
090        break;
091      default:
092        throw new IllegalArgumentException("Unsupported "+service.tut.pori.contentanalysis.Definitions.ELEMENT_TASK_TYPE+": "+response.getTaskType().name());
093    }
094  }
095
096  /**
097   * 
098   * @param authenticatedUser
099   * @param dataGroups
100   * @param limits
101   * @param sortOptions
102   * @return list of media objects or null if none was found
103   */
104  public static MediaObjectList retrieveTagsForUser(UserIdentity authenticatedUser, DataGroups dataGroups, Limits limits, SortOptions sortOptions) {
105    return ServiceInitializer.getDAOHandler().getSolrDAO(MediaObjectDAO.class).search(authenticatedUser, dataGroups, limits, MEDIA_TYPES_TJ, SERVICE_TYPES_TJ, sortOptions, null, null);
106  }
107  
108  /**
109   * Create and schedule twitter summarization task with the given details. If details have taskId given, the task will not be re-added, and will simply be (re-)scheduled.
110   * 
111   * If the details contains no back-ends, default back-ends will be added. See {@link #DEFAULT_CAPABILITIES}
112   * 
113   * @param details details of the task, if profile object is given, it will be ignored.
114   * @return the id of the generated task or null on failure
115   */
116  public static Long summarize(TwitterSummarizationTaskDetails details) {
117    Long taskId = details.getTaskId();
118    if(taskId != null){
119      LOGGER.debug("Task id was given, will not add task.");
120    }else{
121      BackendStatusList backends = details.getBackends();
122      if(BackendStatusList.isEmpty(backends)){
123        LOGGER.debug("No back-ends given, using defaults...");
124        List<AnalysisBackend> ends = ServiceInitializer.getDAOHandler().getSQLDAO(BackendDAO.class).getBackends(DEFAULT_CAPABILITIES);
125        if(ends == null){
126          LOGGER.warn("Aborting task, no capable back-ends.");
127          return null;
128        }
129        
130        backends = new BackendStatusList();
131        backends.setBackendStatus(ends, TaskStatus.NOT_STARTED);
132        details.setBackends(backends);
133      }
134      
135      taskId = ServiceInitializer.getDAOHandler().getSQLDAO(TwitterTaskDAO.class).insertTask(details);
136      if(taskId == null){
137        LOGGER.error("Task schedule failed: failed to insert new task.");
138        return null;
139      }
140    }
141    
142    JobDataMap data = new JobDataMap();
143    TwitterSummarizationTask.setTaskId(data, taskId);
144    LOGGER.debug("Scheduling task, id: "+taskId);
145    JobBuilder builder = JobBuilder.newJob(TwitterSummarizationTask.class);
146    builder.setJobData(data);
147    if(CAContentCore.schedule(builder)){
148      return taskId;
149    }else{
150      LOGGER.warn("Failed to schedule task, id: "+taskId);
151      return null;
152    }
153  }
154  
155  /**
156   * Create and schedule twitter summarization task(s) with the given details. A separate task is created for each of the given screen names. If no screen names are given, task is generated for the authenticated user's twitter account.
157   * 
158   * @param authenticatedUser 
159   * @param contentTypes
160   * @param screenNames
161   * @param summarize
162   * @param synchronize
163   */
164  public static void summarize(UserIdentity authenticatedUser, Set<ContentType> contentTypes, Collection<String> screenNames, boolean summarize, boolean synchronize){
165    if(!UserIdentity.isValid(authenticatedUser)){
166      LOGGER.warn("Invalid user.");
167      return;
168    }
169    if((!summarize && !synchronize) || (contentTypes == null || contentTypes.isEmpty())){
170      LOGGER.warn("Ignored no-op task: no summarize or synchronize requested, or no content types.");
171      return;
172    }
173    
174    TwitterSummarizationTaskDetails details = new TwitterSummarizationTaskDetails();
175    details.setUserId(authenticatedUser);
176    details.setContentTypes(contentTypes);
177    details.setSummarize(summarize);
178    details.setSynchronize(synchronize);
179    
180    if(screenNames == null || screenNames.isEmpty()){
181      LOGGER.debug("No screen names.");
182      TJContentCore.summarize(details);
183    }else{
184      LOGGER.debug("Screen names given, generating "+screenNames.size()+" tasks.");
185      for(String screenName : screenNames){
186        details.setScreenName(screenName);
187        TJContentCore.summarize(details);
188      }
189    }
190  }
191
192  /**
193   * 
194   * @param authenticatedUser
195   * @param rankedObjects
196   * @throws NumberFormatException
197   * @throws IllegalArgumentException
198   */
199  public static void setRanks(UserIdentity authenticatedUser, MediaObjectList rankedObjects) {
200    if(!MediaObjectList.isValid(rankedObjects)){
201      throw new IllegalArgumentException("Invalid media object list.");
202    }
203
204    if(!UserIdentity.isValid(authenticatedUser)){
205      LOGGER.warn("Invalid user.");
206      return;
207    }
208    
209    List<MediaObject> objects = rankedObjects.getMediaObjects();
210    for(MediaObject object : objects){
211      String mediaObjectId = object.getMediaObjectId();
212      if(!UserIdentity.equals(authenticatedUser, object.getOwnerUserId())){
213        LOGGER.debug("User ids do not match for media object id: "+mediaObjectId+", user, id: "+authenticatedUser.getUserId());
214        throw new IllegalArgumentException("Bad media object id.");
215      }
216    }
217    
218    if(!ServiceInitializer.getDAOHandler().getSolrDAO(PhotoDAO.class).update(rankedObjects)){
219      LOGGER.warn("Failed to update media objects.");
220    }
221  }
222  
223  /**
224   * 
225   * @return thread-safe factory instance for the twitter properties
226   */
227  protected static TwitterFactory getTwitterFactory(){
228    TwitterFactory _factory = TWITTER_FACTORY;
229    if(_factory == null){
230      synchronized (TwitterExtractor.class) {
231        if(TWITTER_FACTORY != null){
232          _factory = TWITTER_FACTORY;
233        }else{
234          TwitterProperties tp = ServiceInitializer.getPropertyHandler().getSystemProperties(TwitterProperties.class);
235          LOGGER.debug("Initializing a new twitter factory...");
236          boolean debug = tp.isDebugEnabled();
237          if(debug){
238            LOGGER.debug("Debug enabled.");
239          }
240          
241          Configuration configuration = new ConfigurationBuilder()
242          .setDebugEnabled(debug)
243          .setIncludeEntitiesEnabled(true)
244          .setOAuthConsumerKey(tp.getApiKey())
245          .setOAuthConsumerSecret(tp.getClientSecret())
246          .build();
247          _factory = TWITTER_FACTORY = new TwitterFactory(configuration);
248        }
249      } // synchronized
250    } // if
251    return _factory;
252  }
253
254  /**
255   * Listener for user related events.
256   *
257   * Automatically instantiated by Spring as a bean.
258   */
259  @SuppressWarnings("unused")
260  private static class UserEventListener implements ApplicationListener<UserEvent>{
261
262    @Override
263    public void onApplicationEvent(UserEvent event) {
264      if(event.getType() == EventType.USER_AUTHORIZATION_REVOKED && event.getSource().equals(TwitterUserCore.class)){
265        Long userId = event.getUserId().getUserId();
266        LOGGER.debug("Detected event of type "+EventType.USER_AUTHORIZATION_REVOKED.name()+", scheduling removal of weight modifiers for user, id: "+userId);
267
268        JobDataMap data = new JobDataMap();
269        data.put(JOB_KEY_USER_ID, userId);
270        JobBuilder builder = JobBuilder.newJob(TagRemovalJob.class);
271        builder.setJobData(data);
272        CAContentCore.schedule(builder);
273      }
274    }
275  } // class UserEventListener
276  
277  /**
278   * A job for removing all user content generated by TwitterJazz service.
279   *
280   */
281  public static class TagRemovalJob implements Job{
282
283    @Override
284    public void execute(JobExecutionContext context) throws JobExecutionException {
285      JobDataMap data = context.getMergedJobDataMap();
286      Long userId = data.getLong(JOB_KEY_USER_ID);
287      LOGGER.debug("Removing all content for user, id: "+userId);
288      MediaObjectDAO vDAO = ServiceInitializer.getDAOHandler().getSolrDAO(MediaObjectDAO.class);
289      MediaObjectList mediaObjects = vDAO.search(new UserIdentity(userId), null, null, MEDIA_TYPES_TJ, SERVICE_TYPES_TJ, null , new long[]{userId}, null);
290      if(MediaObjectList.isEmpty(mediaObjects)){
291        LOGGER.debug("User, id: "+userId+" has no media objects.");
292        return;
293      }
294      List<String> remove = mediaObjects.getMediaObjectIds();
295      if(!vDAO.remove(remove)){
296        LOGGER.debug("Failed to remove objects for user, id: "+userId);
297      }
298    }
299  } // class TagRemovalJob
300}