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}