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.EnumSet; 021import java.util.HashMap; 022import java.util.List; 023import java.util.Map; 024 025import org.apache.commons.lang3.ArrayUtils; 026import org.apache.commons.lang3.StringUtils; 027import org.apache.log4j.Logger; 028import org.quartz.Job; 029import org.quartz.JobBuilder; 030import org.quartz.JobDataMap; 031import org.quartz.JobExecutionContext; 032import org.quartz.JobExecutionException; 033import org.springframework.context.ApplicationListener; 034 035import service.tut.pori.contentanalysis.AbstractTaskDetails; 036import service.tut.pori.contentanalysis.AccessDetails; 037import service.tut.pori.contentanalysis.AccessDetails.Permission; 038import service.tut.pori.contentanalysis.AnalysisBackend; 039import service.tut.pori.contentanalysis.AsyncTask; 040import service.tut.pori.contentanalysis.AsyncTask.TaskStatus; 041import service.tut.pori.contentanalysis.AsyncTask.TaskType; 042import service.tut.pori.contentanalysis.BackendDAO; 043import service.tut.pori.contentanalysis.BackendStatus; 044import service.tut.pori.contentanalysis.BackendStatusList; 045import service.tut.pori.contentanalysis.CAContentCore; 046import service.tut.pori.contentanalysis.CAContentCore.ServiceType; 047import service.tut.pori.contentanalysis.Media; 048import service.tut.pori.contentanalysis.Photo; 049import service.tut.pori.contentanalysis.PhotoDAO; 050import service.tut.pori.contentanalysis.PhotoList; 051import service.tut.pori.contentanalysis.PhotoTaskDetails; 052import service.tut.pori.contentanalysis.TaskDAO; 053import service.tut.pori.contentanalysis.video.Video; 054import service.tut.pori.contentanalysis.video.VideoList; 055import service.tut.pori.contentanalysis.video.VideoTaskDetails; 056import service.tut.pori.users.facebook.FacebookUserCore; 057import service.tut.pori.users.google.GoogleUserCore; 058import service.tut.pori.users.twitter.TwitterUserCore; 059import core.tut.pori.context.ServiceInitializer; 060import core.tut.pori.http.RedirectResponse; 061import core.tut.pori.users.UserEvent; 062import core.tut.pori.users.UserEvent.EventType; 063import core.tut.pori.users.UserIdentity; 064import core.tut.pori.utils.ListUtils; 065import core.tut.pori.utils.MediaUrlValidator; 066import core.tut.pori.utils.MediaUrlValidator.MediaType; 067 068/** 069 * The core methods for managing metadata content storage. 070 */ 071public final class ContentStorageCore { 072 private static final String JOB_KEY_SERVICE_TYPES = "serviceType"; // type is EnumSet<ServiceType> 073 private static final String JOB_KEY_USER_ID = "userId"; // type is Long 074 private static final Logger LOGGER = Logger.getLogger(ContentStorageCore.class); 075 076 /** 077 * 078 */ 079 private ContentStorageCore(){ 080 // nothing needed 081 } 082 083 /** 084 * 085 * @param backendIds 086 * @param status 087 * @return all of the requested back-ends with the given status 088 * @throws IllegalArgumentException on invalid id or if no back-ends are available 089 */ 090 private static BackendStatusList getBackendStatuses(int[] backendIds, TaskStatus status) throws IllegalArgumentException { 091 BackendStatusList backendStatusList = new BackendStatusList(); 092 BackendDAO backendDAO = ServiceInitializer.getDAOHandler().getSQLDAO(BackendDAO.class); 093 if(!ArrayUtils.isEmpty(backendIds)){ 094 LOGGER.debug("Adding back-ends..."); 095 List<Integer> backendIdList = ListUtils.createList(backendIds); 096 List<AnalysisBackend> backends = backendDAO.getBackends(backendIdList); 097 if(backends == null){ 098 throw new IllegalArgumentException("Invalid back-end ids."); 099 } 100 for(Integer backendId : backendIdList){ // validate the user given back-end ids 101 AnalysisBackend end = null; 102 for(AnalysisBackend backend : backends){ 103 if(backend.getBackendId().equals(backendId)){ 104 end = backend; 105 break; 106 } 107 } 108 if(end == null){ 109 throw new IllegalArgumentException("Invalid back-end id: "+backendId); 110 } 111 backendStatusList.setBackendStatus(new BackendStatus(end, status)); 112 } 113 }else{ 114 LOGGER.debug("No back-ends given, attempting to use defaults..."); 115 List<AnalysisBackend> backends = backendDAO.getEnabledBackends(); 116 if(backends == null){ 117 throw new IllegalArgumentException("No enabled back-ends available."); 118 } 119 for(AnalysisBackend end : backends){ 120 backendStatusList.setBackendStatus(new BackendStatus(end, status)); 121 } 122 } 123 return backendStatusList; 124 } 125 126 /** 127 * 128 * @param authenticatedUser 129 * @param backendIds 130 * @param serviceTypes 131 * @return task id of the scheduled task or null if schedule failed 132 */ 133 public static Long synchronize(UserIdentity authenticatedUser, int[] backendIds, EnumSet<ServiceType> serviceTypes) { 134 SynchronizationTaskDetails details = new SynchronizationTaskDetails(); 135 details.setBackends(getBackendStatuses(backendIds, TaskStatus.NOT_STARTED)); 136 SynchronizationTaskDetails.setServiceTypes(details, serviceTypes); 137 details.setUserId(authenticatedUser); 138 139 Long taskId = ServiceInitializer.getDAOHandler().getSQLDAO(TaskDAO.class).insertTask(details); // use the default implementation to insert as there is only metadata content 140 if(taskId == null){ 141 LOGGER.warn("Failed to add new synchronization task."); 142 }else{ 143 CAContentCore.scheduleTask(JobBuilder.newJob(MetadataSynchronizationJob.class), taskId); 144 } 145 return taskId; 146 } 147 148 /** 149 * resolves dynamic /rest/r? redirection URL to static access URL 150 * 151 * @param authenticatedUser 152 * @param serviceType 153 * @param guid 154 * @return redirection to dynamic URL 155 */ 156 public static RedirectResponse generateTargetUrl(UserIdentity authenticatedUser, ServiceType serviceType, String guid){ 157 AccessDetails details = ServiceInitializer.getDAOHandler().getSolrDAO(PhotoDAO.class).getAccessDetails(authenticatedUser, guid); 158 if(details == null){ 159 throw new IllegalArgumentException("Not Found."); 160 } 161 Permission access = details.getPermission(); 162 if(access == Permission.NO_ACCESS){ 163 LOGGER.debug("Access denied for GUID: "+guid+" for userId: "+(UserIdentity.isValid(authenticatedUser) ? authenticatedUser.getUserId() : "none")); 164 throw new IllegalArgumentException("Not Found."); 165 } 166 LOGGER.debug("Granting access with "+Permission.class.toString()+" : "+access.name()); 167 168 String url = getContentStorage(false, serviceType).getTargetUrl(details); 169 if(url == null){ 170 throw new IllegalArgumentException("Not Found."); 171 }else{ 172 return new RedirectResponse(url); 173 } 174 } 175 176 /** 177 * 178 * @param autoScheduleEnabled 179 * @param serviceType 180 * @return content storage for the given service type 181 * @throws UnsupportedOperationException 182 */ 183 public static final ContentStorage getContentStorage(boolean autoScheduleEnabled, ServiceType serviceType) throws UnsupportedOperationException{ 184 switch(serviceType){ 185 case PICASA_STORAGE_SERVICE: 186 return new PicasaCloudStorage(autoScheduleEnabled); 187 case FACEBOOK_PHOTO: 188 return new FacebookPhotoStorage(autoScheduleEnabled); 189 case TWITTER_PHOTO: 190 return new TwitterPhotoStorage(autoScheduleEnabled); 191 case URL_STORAGE: 192 return new URLContentStorage(autoScheduleEnabled); 193 default: 194 throw new UnsupportedOperationException("Unsupported ServiceType: "+serviceType.name()); 195 } 196 } 197 198 /** 199 * Removes all (photo) metadata associated with the given user. 200 * This contains all service specific entries and the related media and media objects. 201 * 202 * @param authenticatedUser 203 * @param guids optional filter 204 * @param serviceTypes 205 */ 206 public static void removeMetadata(UserIdentity authenticatedUser, Collection<String> guids, EnumSet<ServiceType> serviceTypes){ 207 if(ServiceType.isEmpty(serviceTypes)){ 208 LOGGER.warn("No service types given."); 209 return; 210 } 211 212 for(ServiceType type : serviceTypes){ 213 try{ 214 getContentStorage(true, type).removeMetadata(authenticatedUser, guids); 215 }catch(UnsupportedOperationException ex){ 216 LOGGER.warn(ex, ex); 217 } 218 } // for 219 } 220 221 /** 222 * 223 * @param authenticatedUser 224 * @param backendIds 225 * @param urls 226 * @return details of the files added to the analysis task, note that not all files are necessary new ones, if the given URLs were already known by the system 227 */ 228 public static MediaList addUrls(UserIdentity authenticatedUser, int[] backendIds, List<String> urls) { 229 List<String> photoUrls = new ArrayList<>(); 230 List<String> videoUrls = new ArrayList<>(); 231 MediaUrlValidator validator = new MediaUrlValidator(); 232 for(String url : urls){ 233 switch(validator.validateUrl(url)){ 234 case PHOTO: 235 LOGGER.debug("Detected photo: "+url); 236 photoUrls.add(url); 237 break; 238 case VIDEO: 239 LOGGER.debug("Detected video: "+url); 240 videoUrls.add(url); 241 break; 242 default: 243 LOGGER.warn("Unknown media type for URL: "+url); 244 break; 245 } 246 } 247 248 BackendStatusList backends = getBackendStatuses(backendIds, TaskStatus.NOT_STARTED); 249 URLContentStorage storage = new URLContentStorage(); 250 storage.setBackends(backends); 251 ContentStorageListener listener = new ContentStorageListener(); 252 storage.setContentStorageListener(listener); 253 254 if(photoUrls.isEmpty()){ 255 LOGGER.debug("No photo URLs."); 256 }else{ 257 storage.addUrls(MediaType.PHOTO, authenticatedUser, photoUrls); 258 } 259 260 if(videoUrls.isEmpty()){ 261 LOGGER.debug("No video URLs."); 262 }else{ 263 storage.addUrls(MediaType.VIDEO, authenticatedUser, videoUrls); 264 } 265 266 return listener.getMediaList(); 267 } 268 269 /** 270 * Listener for user related events. 271 * 272 * Automatically instantiated by Spring as a bean. 273 */ 274 @SuppressWarnings("unused") 275 private static class UserEventListener implements ApplicationListener<UserEvent>{ 276 277 @Override 278 public void onApplicationEvent(UserEvent event) { 279 EventType type = event.getType(); 280 switch(type){ 281 case USER_AUTHORIZATION_REVOKED: 282 Long userId = event.getUserId().getUserId(); 283 LOGGER.debug("Detected event of type "+type.name()+", scheduling removal of all metadata content for user, id: "+userId); 284 Class<?> source = event.getSource(); 285 if(source == FacebookUserCore.class){ 286 createJob(userId, EnumSet.of(ServiceType.FACEBOOK_PHOTO)); 287 }else if(source == GoogleUserCore.class){ 288 createJob(userId, EnumSet.of(ServiceType.PICASA_STORAGE_SERVICE)); 289 }else if(source == TwitterUserCore.class){ 290 createJob(userId, EnumSet.of(ServiceType.TWITTER_PHOTO)); 291 } 292 break; 293 case USER_REMOVED: 294 userId = event.getUserId().getUserId(); 295 LOGGER.debug("Detected event of type "+type.name()+", scheduling removal of all metadata content for user, id: "+userId); 296 createJob(userId, EnumSet.allOf(ServiceType.class)); 297 break; 298 default: // ignore everything else 299 break; 300 } 301 } 302 303 /** 304 * 305 * @param userId 306 * @param serviceTypes 307 */ 308 private void createJob(Long userId, EnumSet<ServiceType> serviceTypes){ 309 JobBuilder builder = JobBuilder.newJob(MetadataRemovalJob.class); 310 JobDataMap data = new JobDataMap(); 311 data.put(JOB_KEY_USER_ID, userId); 312 data.put(JOB_KEY_SERVICE_TYPES, serviceTypes); 313 builder.setJobData(data); 314 CAContentCore.schedule(builder); 315 } 316 } // class UserEventListener 317 318 /** 319 * Job for removing content for the user designated by data key JOB_KEY_USER_ID for services designated by data key JOB_KEY_SERVICE_TYPES 320 * 321 */ 322 public static class MetadataRemovalJob implements Job{ 323 324 @SuppressWarnings("unchecked") 325 @Override 326 public void execute(JobExecutionContext context) throws JobExecutionException { 327 JobDataMap data = context.getMergedJobDataMap(); 328 Long userId = data.getLong(JOB_KEY_USER_ID); 329 LOGGER.debug("Removing all metadata content for user, id: "+userId); 330 removeMetadata(new UserIdentity(userId), null, (EnumSet<ServiceType>) data.get(JOB_KEY_SERVICE_TYPES)); 331 } 332 } // class MetadataRemovalJob 333 334 /** 335 * Implementation of AbstractTaskDetails used internally for scheduling synchronization tasks. 336 */ 337 public static class SynchronizationTaskDetails extends AbstractTaskDetails{ 338 private static final String METADATA_SERVICE_TYPES = "serviceTypes"; 339 340 /** 341 * 342 */ 343 public SynchronizationTaskDetails(){ 344 super(); 345 setTaskType(TaskType.UNDEFINED); 346 } 347 348 /** 349 * 350 * @param details 351 * @param serviceTypes 352 */ 353 public static void setServiceTypes(AbstractTaskDetails details, EnumSet<ServiceType> serviceTypes){ 354 Map<String, String> metadata = details.getMetadata(); 355 if(serviceTypes == null || serviceTypes.isEmpty()){ 356 LOGGER.debug("No service types."); 357 if(metadata != null){ 358 metadata.remove(METADATA_SERVICE_TYPES); 359 if(metadata.isEmpty()){ 360 details.setMetadata(null); 361 } 362 } 363 return; 364 } 365 StringBuilder cb = new StringBuilder(); 366 for(ServiceType s : serviceTypes){ 367 cb.append(s.getServiceId()); 368 cb.append(core.tut.pori.http.Definitions.SEPARATOR_URI_QUERY_PARAM_VALUES); 369 } 370 if(metadata == null){ 371 metadata = new HashMap<>(1); 372 } 373 metadata.put(METADATA_SERVICE_TYPES, cb.substring(0, cb.length()-1)); 374 details.setMetadata(metadata); 375 } 376 377 /** 378 * 379 * @param details 380 * @return service types associated with the details 381 */ 382 public static EnumSet<ServiceType> getServiceTypes(AbstractTaskDetails details){ 383 Map<String, String> metadata = details.getMetadata(); 384 if(metadata == null || metadata.isEmpty()){ 385 LOGGER.debug("No metadata."); 386 return null; 387 } 388 389 String[] serviceTypes = StringUtils.split(metadata.get(METADATA_SERVICE_TYPES), core.tut.pori.http.Definitions.SEPARATOR_URI_QUERY_PARAM_VALUES); 390 if(serviceTypes == null){ 391 LOGGER.debug("No service names."); 392 return null; 393 } 394 395 EnumSet<ServiceType> types = EnumSet.noneOf(ServiceType.class); 396 for(int i=0;i<serviceTypes.length;++i){ 397 types.add(ServiceType.fromServiceId(Integer.valueOf(serviceTypes[i]))); 398 } 399 return types; 400 } 401 402 @Override 403 public TaskParameters getTaskParameters() { 404 return null; 405 } 406 407 @Override 408 public void setTaskParameters(TaskParameters parameters) throws UnsupportedOperationException { 409 throw new UnsupportedOperationException("Method not supported."); 410 } 411 } // class SynchronizationTaskDetails 412 413 /** 414 * A schedulable task used for synchronizing metadata 415 */ 416 public static class MetadataSynchronizationJob implements Job{ 417 418 @Override 419 public void execute(JobExecutionContext context) throws JobExecutionException { 420 JobDataMap data = context.getMergedJobDataMap(); 421 Long taskId = AsyncTask.getTaskId(data); 422 if(taskId == null){ 423 LOGGER.warn("No taskId."); 424 return; 425 } 426 427 TaskDAO taskDAO = ServiceInitializer.getDAOHandler().getSQLDAO(TaskDAO.class); 428 BackendStatusList backends = taskDAO.getBackendStatus(taskId, TaskStatus.NOT_STARTED); 429 if(BackendStatusList.isEmpty(backends)){ 430 LOGGER.warn("No analysis back-ends available for taskId: "+taskId+" with status "+TaskStatus.NOT_STARTED.name()); 431 return; 432 } 433 434 AbstractTaskDetails details = taskDAO.getTask(null, null, null, taskId); // no need to retrieve per back-end as the details are the same for each back-end 435 if(details == null){ 436 LOGGER.warn("Task not found, id: "+taskId); 437 return; 438 } 439 440 UserIdentity userId = details.getUserId(); 441 LOGGER.debug("Execution started for user id: "+userId.getUserId()); 442 443 for(ServiceType type : SynchronizationTaskDetails.getServiceTypes(details)){ 444 try{ 445 ContentStorage storage = getContentStorage(true, type); 446 storage.setBackends(backends); 447 if(!storage.synchronizeAccount(userId)){ 448 LOGGER.warn("Failed to synchronize service of type "+type.name()+" for user, id: "+userId.getUserId()); 449 } 450 }catch(Throwable ex){ // catch exceptions to prevent re-scheduling of the task on error 451 LOGGER.warn(ex, ex); 452 } 453 } 454 LOGGER.debug("Synchronization completed."); 455 } 456 } // class MetadataSynchronizationJob 457 458 /** 459 * internally used listener class 460 * 461 */ 462 private static class ContentStorageListener implements service.tut.pori.contentstorage.ContentStorage.ContentStorageListener { 463 private PhotoList _analysisTaskPhotoList = null; 464 private VideoList _analysisTaskVideoList = null; 465 466 @Override 467 public void analysisTaskCreated(AbstractTaskDetails details) { 468 if(details != null){ 469 if(details instanceof PhotoTaskDetails){ 470 _analysisTaskPhotoList = ((PhotoTaskDetails) details).getPhotoList(); 471 }else if(details instanceof VideoTaskDetails){ 472 _analysisTaskVideoList = ((VideoTaskDetails) details).getVideoList(); 473 }else{ 474 LOGGER.debug("Ignored unsupported task details of type "+details.getClass()); 475 } 476 }else{ 477 LOGGER.warn("Received null task details."); 478 } 479 } 480 481 @Override 482 public void feedbackTaskCreated(AbstractTaskDetails details) { 483 // nothing needed 484 } 485 486 /** 487 * 488 * @return combined media list of task photos and videos 489 */ 490 public MediaList getMediaList() { 491 if(_analysisTaskPhotoList == null && _analysisTaskVideoList == null){ 492 return null; 493 } 494 495 List<Media> media = new ArrayList<>(PhotoList.count(_analysisTaskPhotoList)+VideoList.count(_analysisTaskVideoList)); 496 if(_analysisTaskPhotoList != null){ 497 for(Photo p : _analysisTaskPhotoList.getPhotos()){ 498 media.add(p); 499 } 500 } 501 if(_analysisTaskVideoList != null){ 502 for(Video v : _analysisTaskVideoList.getVideos()){ 503 media.add(v); 504 } 505 } 506 507 MediaList mediaList = new MediaList(); 508 mediaList.setMedia(media); 509 return mediaList; 510 } 511 } // class ContentStorageListener 512}