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.io.IOException;
019import java.util.EnumSet;
020import java.util.Iterator;
021import java.util.List;
022
023import org.apache.commons.lang3.StringUtils;
024import org.apache.http.client.methods.HttpPost;
025import org.apache.http.entity.StringEntity;
026import org.apache.http.impl.client.BasicResponseHandler;
027import org.apache.http.impl.client.CloseableHttpClient;
028import org.apache.http.impl.client.HttpClients;
029import org.apache.log4j.Logger;
030import org.quartz.JobDataMap;
031import org.quartz.JobExecutionContext;
032
033import service.tut.pori.contentanalysis.AnalysisBackend;
034import service.tut.pori.contentanalysis.AnalysisBackend.Capability;
035import service.tut.pori.contentanalysis.AsyncTask;
036import service.tut.pori.contentanalysis.BackendStatus;
037import service.tut.pori.contentanalysis.BackendStatusList;
038import service.tut.pori.contentanalysis.CAContentCore.ServiceType;
039import service.tut.pori.contentanalysis.Definitions;
040import service.tut.pori.contentanalysis.Photo;
041import service.tut.pori.contentanalysis.CAContentCore.Visibility;
042import service.tut.pori.contentanalysis.CAContentCore;
043import service.tut.pori.contentanalysis.PhotoDAO;
044import service.tut.pori.contentanalysis.PhotoList;
045import service.tut.pori.contentanalysis.MediaObject;
046import service.tut.pori.contentanalysis.MediaObjectDAO;
047import service.tut.pori.contentanalysis.MediaObjectList;
048import service.tut.pori.contentstorage.FacebookPhotoStorage;
049import core.tut.pori.context.ServiceInitializer;
050import core.tut.pori.users.UserIdentity;
051import core.tut.pori.utils.XMLFormatter;
052
053
054/**
055 * An implementation of ASyncTask, meant for executing a Facebook summarization task.
056 * 
057 * Requires a valid taskId for execution, provided in a JobExecutionContext. Optionally, backendIdList can be provided.
058 *
059 */
060public class FBSummarizationTask extends AsyncTask{
061  private static final Double DEFAULT_TAG_CONFIDENCE = 1.0;
062  private static final Integer DEFAULT_TAG_RANK = 1;
063  private static final Logger LOGGER = Logger.getLogger(FBSummarizationTask.class);
064
065  @Override
066  public void execute(JobExecutionContext context) {
067    try{
068      LOGGER.debug("Executing task...");
069      JobDataMap data = context.getMergedJobDataMap();
070
071      Long taskId = getTaskId(data);
072      if(taskId == null){
073        LOGGER.warn("No taskId.");
074        return;
075      }
076
077      FBTaskDAO taskDAO = ServiceInitializer.getDAOHandler().getSQLDAO(FBTaskDAO.class);
078      BackendStatusList backends = taskDAO.getBackendStatus(taskId, TaskStatus.NOT_STARTED);
079      if(BackendStatusList.isEmpty(backends)){
080        LOGGER.warn("No analysis back-ends available for taskId: "+taskId+" with status "+TaskStatus.NOT_STARTED.name());
081        return;
082      }
083
084      FBSummarizationTaskDetails details = (FBSummarizationTaskDetails) taskDAO.getTask(null, null, null, taskId); // no need to retrive per back-end as the details are the same for each back-end
085      if(details == null){
086        LOGGER.warn("Task not found, id: "+taskId);
087        return;
088      }
089
090      UserIdentity userId = details.getUserId();
091      if(details.isSynchronize()){
092        LOGGER.debug("Synchronizing...");
093        new FacebookPhotoStorage().synchronizeAccount(userId); // synchronize the account ignoring possible errors, there is no need to wait for the generated photo analysis tasks to finish
094      }else{
095        LOGGER.debug("Not synchronizing...");
096      }
097      
098      backends = BackendStatusList.getBackendStatusList(backends.getBackendStatuses(EnumSet.of(Capability.FACEBOOK_SUMMARIZATION))); // filter out incapable back-ends
099      if(BackendStatusList.isEmpty(backends)){
100        LOGGER.warn("Aborting summarization... no back-ends with capability "+Capability.FACEBOOK_SUMMARIZATION.name());
101        return;
102      }
103
104      FacebookExtractor e = FacebookExtractor.getExtractor(userId);
105      if(e == null){
106        LOGGER.error("Failed to create extractor for the given user.");
107        return;
108      }
109
110      FacebookProfile p = e.getProfile(details.getContentTypes());
111      if(p == null){
112        LOGGER.error("Failed to retrieve profile for the given user from Facebook.");
113        return;
114      }
115
116      details.setProfile(p);
117
118      try (CloseableHttpClient client = HttpClients.createDefault()) {
119        BasicResponseHandler h = new BasicResponseHandler();
120        for(BackendStatus status : backends.getBackendStatuses()){
121          AnalysisBackend end = status.getBackend();
122          try {
123            Integer backendId = end.getBackendId();
124            String url = end.getAnalysisUri()+Definitions.METHOD_ADD_TASK;
125            LOGGER.debug("Task, id: "+taskId+", back-end id: "+backendId+". Executing POST "+url);
126            HttpPost taskRequest = new HttpPost(url);
127            details.setBackendId(backendId);
128            taskRequest.setHeader("Content-Type", "text/xml; charset=UTF-8");
129            taskRequest.setEntity(new StringEntity((new XMLFormatter()).toString(details), core.tut.pori.http.Definitions.ENCODING_UTF8));        
130
131            LOGGER.debug("Backend with id: "+backendId+" responded "+client.execute(taskRequest,h));
132          } catch (IOException ex) {
133            LOGGER.warn(ex, ex);
134          }
135        }
136      } catch (IOException ex) {
137        LOGGER.error(ex, ex);
138      }
139    } catch(Throwable ex){  // catch all exceptions to prevent re-scheduling on error
140      LOGGER.error(ex, ex);
141    }
142  }
143
144  /**
145   * Process the response. After this method has finished, the response will not contain non-existent photos (if any were present).
146   * 
147   * @param response
148   * @throws IllegalArgumentException on bad data
149   */
150  public static void taskFinished(FBTaskResponse response) throws IllegalArgumentException {
151    Integer backendId = response.getBackendId();
152    if(backendId == null){
153      throw new IllegalArgumentException("Invalid backendId.");
154    }
155    Long taskId = response.getTaskId();
156    if(taskId == null){
157      throw new IllegalArgumentException("Invalid taskId.");
158    }
159
160    FBTaskDAO taskDAO = ServiceInitializer.getDAOHandler().getSQLDAO(FBTaskDAO.class);
161    BackendStatus backendStatus = taskDAO.getBackendStatus(backendId, taskId);
162    if(backendStatus == null){
163      LOGGER.warn("Backend, id: "+backendId+" returned results for task, not given to the backend. TaskId: "+taskId);
164      throw new IllegalArgumentException("This task is not given for backend, id: "+backendId);
165    }
166
167    TaskStatus status = response.getStatus();
168    if(status == null){
169      LOGGER.warn("Task status not available.");
170      status = TaskStatus.UNKNOWN;
171    }
172    backendStatus.setStatus(status);
173
174    try{
175      PhotoList photoList = response.getPhotoList();
176      if(PhotoList.isEmpty(photoList)){
177        LOGGER.debug("No photo list returned by backend, id: "+backendId+", task, id: "+taskId);
178      }else{ // create media objects and associate
179        PhotoDAO pdao = ServiceInitializer.getDAOHandler().getSolrDAO(PhotoDAO.class);
180        List<String> foundGUIDs = PhotoList.getGUIDs(pdao.getPhotos(null, photoList.getGUIDs(), null, null, null));
181        if(foundGUIDs == null){
182          LOGGER.warn("None of the photos exist, will not process media objects for backend, id: "+backendId+" for task, id: "+taskId);
183          photoList = null; // prevents generation of feedback task for invalid content
184        }else{
185          List<Photo> photos = photoList.getPhotos();
186          LOGGER.debug("New media objects for photos, photo count: "+photos.size()+", backend, id: "+backendId);
187          for(Iterator<Photo> iter = photos.iterator(); iter.hasNext();){
188            Photo photo = iter.next();
189            MediaObjectList mediaObjects = photo.getMediaObjects();
190            if(MediaObjectList.isEmpty(mediaObjects)){
191              LOGGER.warn("Ignored empty media object list for backend, id: "+backendId+" for task, id: "+taskId+", photo, GUID: "+photo.getGUID());
192              iter.remove();
193            }else if(!foundGUIDs.contains(photo.getGUID())){
194              LOGGER.warn("Ignored non-existing photo for backend, id: "+backendId+" for task, id: "+taskId+", photo, GUID: "+photo.getGUID());
195              iter.remove(); // remove to prevent association
196            }else if(!validate(mediaObjects, backendId, photo.getOwnerUserId()) || !insertOrUpdate(mediaObjects)){
197              backendStatus.setStatus(TaskStatus.ERROR);
198              throw new IllegalArgumentException("Invalid object list returned by backend, id: "+backendId+" for task, id: "+taskId);
199            }
200          } // for
201          pdao.associate(photoList);
202        } // else
203      }
204
205      MediaObjectList objects = response.getMediaObjects();
206      if(MediaObjectList.isEmpty(objects)){
207        LOGGER.debug("No media object list returned by backend, id: "+backendId+" for task, id: "+taskId);
208      }else if(!validate(objects, backendId, null) || !insertOrUpdate(objects)){
209        backendStatus.setStatus(TaskStatus.ERROR);
210        throw new IllegalArgumentException("Invalid object list returned by backend, id: "+backendId+" for task, id: "+taskId);
211      }else{
212        LOGGER.debug("New media objects: "+objects.getMediaObjects().size()+" backend, id: "+backendId);
213      }
214
215      CAContentCore.scheduleBackendFeedback(backendId, photoList, taskId);
216    } finally {
217      taskDAO.updateTaskStatus(backendStatus, taskId);
218      ServiceInitializer.getEventHandler().publishEvent(new AsyncTaskEvent(backendId, FBSummarizationTask.class, status, taskId, TaskType.FACEBOOK_PROFILE_SUMMARIZATION));
219    }
220  }
221
222  /**
223   * 
224   * @param mediaObjects non-null, non-empty validated object list
225   * @return true on success
226   */
227  private static boolean insertOrUpdate(MediaObjectList mediaObjects){
228    MediaObjectList updates = new MediaObjectList();
229    MediaObjectList inserts = new MediaObjectList();
230
231    for(MediaObject o : mediaObjects.getMediaObjects()){
232      if(StringUtils.isBlank(o.getMediaObjectId())){ // no media object id
233        inserts.addMediaObject(o);
234      }else{
235        updates.addMediaObject(o);
236      }
237    }
238    
239    PhotoDAO photoDAO = ServiceInitializer.getDAOHandler().getSolrDAO(PhotoDAO.class);
240    if(MediaObjectList.isEmpty(inserts)){
241      LOGGER.debug("Nothing to insert.");
242    }else if(!photoDAO.insert(inserts)){
243      LOGGER.warn("Failed to insert media objects.");
244      return false;
245    }
246
247    if(MediaObjectList.isEmpty(updates)){
248      LOGGER.debug("Nothing to update.");
249    }else if(!photoDAO.update(updates)){
250      LOGGER.warn("Failed to update media objects.");
251      return false;
252    }
253    return true;
254  }
255
256  /**
257   * Validate the given list of media objects, if confidence is missing, this method will automatically set it to default, rank will also be set to 0
258   * 
259   * This also set the correct serviceType and visibility ({service.tut.pori.contentanalysis.CAContentCore.Visibility#PRIVATE}, if not given), and resolves mediaObjectIds
260   * 
261   * @param mediaObjects non-empty and non-null list of objects
262   * @param backendId non-null id
263   * @param userId if null, the check will be ignored
264   * @return true if the given values were valid
265   */
266  private static boolean validate(MediaObjectList mediaObjects, Integer backendId, UserIdentity userId){
267    MediaObjectDAO vdao = ServiceInitializer.getDAOHandler().getSolrDAO(MediaObjectDAO.class);
268    vdao.resolveObjectIds(mediaObjects);
269    for(MediaObject object : mediaObjects.getMediaObjects()){
270      if(backendId != object.getBackendId()){
271        LOGGER.warn("Backend id mismatch.");
272        return false;
273      }else if(userId != null && !UserIdentity.equals(object.getOwnerUserId(), userId)){
274        LOGGER.warn("Media objects user identity does not match the given user identity.");
275        return false;
276      }
277      Integer rank = object.getRank();
278      if(rank == null){
279        object.setRank(DEFAULT_TAG_RANK);
280      }
281      Double confidence = object.getConfidence();
282      if(confidence == null){
283        object.setConfidence(DEFAULT_TAG_CONFIDENCE);
284      }
285      object.setServiceType(ServiceType.FACEBOOK_JAZZ);
286      if(object.getVisibility() == null){
287        object.setVisibility(Visibility.PRIVATE);
288      }
289    }
290    return true;
291  }
292}