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.Iterator; 022import java.util.List; 023 024import org.apache.log4j.Logger; 025 026import service.tut.pori.contentanalysis.AbstractTaskDetails; 027import service.tut.pori.contentanalysis.AccessDetails; 028import service.tut.pori.contentanalysis.AnalysisBackend.Capability; 029import service.tut.pori.contentanalysis.PhotoParameters; 030import service.tut.pori.contentanalysis.PhotoParameters.AnalysisType; 031import service.tut.pori.contentanalysis.AsyncTask.TaskType; 032import service.tut.pori.contentanalysis.CAContentCore; 033import service.tut.pori.contentanalysis.CAContentCore.ServiceType; 034import service.tut.pori.contentanalysis.CAContentCore.Visibility; 035import service.tut.pori.contentanalysis.DeletedPhotoList; 036import service.tut.pori.contentanalysis.Photo; 037import service.tut.pori.contentanalysis.PhotoDAO; 038import service.tut.pori.contentanalysis.PhotoFeedbackTask.FeedbackTaskBuilder; 039import service.tut.pori.contentanalysis.PhotoList; 040import service.tut.pori.contentanalysis.PhotoTaskDetails; 041import service.tut.pori.contentanalysis.video.DeletedVideoList; 042import service.tut.pori.contentanalysis.video.Video; 043import service.tut.pori.contentanalysis.video.VideoContentCore; 044import service.tut.pori.contentanalysis.video.VideoDAO; 045import service.tut.pori.contentanalysis.video.VideoFeedbackTask; 046import service.tut.pori.contentanalysis.video.VideoList; 047import service.tut.pori.contentanalysis.video.VideoParameters; 048import service.tut.pori.contentanalysis.video.VideoTaskDetails; 049import core.tut.pori.context.ServiceInitializer; 050import core.tut.pori.users.UserIdentity; 051import core.tut.pori.utils.MediaUrlValidator; 052import core.tut.pori.utils.MediaUrlValidator.MediaType; 053import core.tut.pori.utils.UserIdentityLock; 054 055/** 056 * <p>Storage handler that supports saving arbitrary URLs to for the analysis. 057 * The URLs must be of valid image content. ImageValidator is used for performing a simple content check.</p> 058 * <p>This storage service does not split tasks into smaller chunks like the other content storage services.</p> 059 */ 060public final class URLContentStorage extends ContentStorage { 061 /** Service type declaration for this storage */ 062 public static final ServiceType SERVICE_TYPE = ServiceType.URL_STORAGE; 063 private static final PhotoParameters ANALYSIS_PARAMETERS_PHOTO; 064 private static final VideoParameters ANALYSIS_PARAMETERS_VIDEO; 065 static{ 066 ANALYSIS_PARAMETERS_PHOTO = new PhotoParameters(); 067 ANALYSIS_PARAMETERS_PHOTO.setAnalysisTypes(EnumSet.of(AnalysisType.FACE_DETECTION, AnalysisType.KEYWORD_EXTRACTION, AnalysisType.VISUAL)); 068 ANALYSIS_PARAMETERS_VIDEO = new VideoParameters(); 069 ANALYSIS_PARAMETERS_VIDEO.setSequenceDuration(1); 070 ANALYSIS_PARAMETERS_VIDEO.setAnalysisTypes(EnumSet.of(AnalysisType.VISUAL, AnalysisType.KEYWORD_EXTRACTION)); 071 } 072 private static final EnumSet<Capability> CAPABILITIES = EnumSet.of(Capability.PHOTO_ANALYSIS); 073 private static final Logger LOGGER = Logger.getLogger(URLContentStorage.class); 074 private static final UserIdentityLock USER_IDENTITY_LOCK = new UserIdentityLock(); 075 076 /** 077 * 078 */ 079 public URLContentStorage(){ 080 super(); 081 } 082 083 /** 084 * 085 * @param autoSchedule 086 */ 087 public URLContentStorage(boolean autoSchedule){ 088 super(autoSchedule); 089 } 090 091 @Override 092 public ServiceType getServiceType() { 093 return SERVICE_TYPE; 094 } 095 096 @Override 097 public EnumSet<Capability> getBackendCapabilities() { 098 return CAPABILITIES; 099 } 100 101 @Override 102 public String getTargetUrl(AccessDetails details) { 103 return ServiceInitializer.getDAOHandler().getSQLDAO(URLContentDAO.class).getUrl(details.getGuid()); 104 } 105 106 @Override 107 public void removeMetadata(UserIdentity userId, Collection<String> guids) { 108 removePhotoMetadata(userId, guids); 109 removeVideoMetadata(userId, guids); 110 } 111 112 /** 113 * 114 * @param userId 115 * @param guids 116 */ 117 public void removePhotoMetadata(UserIdentity userId, Collection<String> guids) { 118 PhotoDAO photoDAO = ServiceInitializer.getDAOHandler().getSolrDAO(PhotoDAO.class); 119 PhotoList photos = photoDAO.getPhotos(null, guids, null, EnumSet.of(SERVICE_TYPE), new long[]{userId.getUserId()}); 120 if(PhotoList.isEmpty(photos)){ 121 LOGGER.debug("User, id: "+userId.getUserId()+" has no photos."); 122 return; 123 } 124 List<String> remove = photos.getGUIDs(); 125 photoDAO.remove(remove); 126 ServiceInitializer.getDAOHandler().getSQLDAO(URLContentDAO.class).removeEntries(remove); 127 128 FeedbackTaskBuilder builder = new FeedbackTaskBuilder(TaskType.FEEDBACK); // create builder for deleted photo feedback task 129 builder.setUser(userId); 130 builder.addDeletedPhotos(DeletedPhotoList.getPhotoList(photos.getPhotos(), photos.getResultInfo())); 131 builder.setBackends(getBackends()); 132 PhotoTaskDetails details = builder.build(); 133 if(details == null){ 134 LOGGER.warn("No content."); 135 }else{ 136 if(isAutoSchedule()){ 137 LOGGER.debug("Scheduling feedback task."); 138 CAContentCore.scheduleTask(details); 139 }else{ 140 LOGGER.debug("Auto-schedule is disabled."); 141 } 142 143 notifyFeedbackTaskCreated(details); 144 } 145 } 146 147 /** 148 * 149 * @param userId 150 * @param guids 151 */ 152 public void removeVideoMetadata(UserIdentity userId, Collection<String> guids) { 153 VideoDAO videoDAO = ServiceInitializer.getDAOHandler().getSolrDAO(VideoDAO.class); 154 VideoList videos = videoDAO.getVideos(null, guids, null, EnumSet.of(SERVICE_TYPE), new long[]{userId.getUserId()}); 155 if(VideoList.isEmpty(videos)){ 156 LOGGER.debug("User, id: "+userId.getUserId()+" has no videos."); 157 return; 158 } 159 List<String> remove = videos.getGUIDs(); 160 videoDAO.remove(remove); 161 ServiceInitializer.getDAOHandler().getSQLDAO(URLContentDAO.class).removeEntries(remove); 162 163 service.tut.pori.contentanalysis.video.VideoFeedbackTask.FeedbackTaskBuilder builder = new service.tut.pori.contentanalysis.video.VideoFeedbackTask.FeedbackTaskBuilder(TaskType.FEEDBACK); // create builder for deleted video feedback task 164 builder.setUser(userId); 165 builder.addDeletedVideos(DeletedVideoList.getVideoList(videos.getVideos(), videos.getResultInfo())); 166 builder.setBackends(getBackends()); 167 VideoTaskDetails details = builder.build(); 168 if(details == null){ 169 LOGGER.warn("No content."); 170 }else{ 171 if(isAutoSchedule()){ 172 LOGGER.debug("Scheduling feedback task."); 173 VideoContentCore.scheduleTask(details); 174 }else{ 175 LOGGER.debug("Auto-schedule is disabled."); 176 } 177 178 notifyFeedbackTaskCreated(details); 179 } 180 } 181 182 /** 183 * this will simply remove all non-existing images 184 */ 185 @Override 186 public boolean synchronizeAccount(UserIdentity userId) { 187 USER_IDENTITY_LOCK.acquire(userId); 188 LOGGER.debug("Synchronizing account for user, id: "+userId.getUserId()); 189 try{ 190 URLContentDAO urlContentDAO = ServiceInitializer.getDAOHandler().getSQLDAO(URLContentDAO.class); 191 List<URLEntry> entries = urlContentDAO.getEntries(null, null, null, userId); 192 MediaUrlValidator validator = new MediaUrlValidator(); 193 for(Iterator<URLEntry> iter = entries.iterator(); iter.hasNext();){ // loop through the entries, leave invalid uris in the list 194 URLEntry e = iter.next(); 195 String url = e.getUrl(); 196 MediaType detectedType = validator.validateUrl(url); 197 if(detectedType == MediaType.UNKNOWN){ 198 LOGGER.debug("Invalid URL detected: "+url); 199 }else if(!detectedType.equals(e.getMediaType())){ 200 LOGGER.warn("Detected type "+detectedType.toInt()+" does not match the stored type "+e.getMediaType().toInt()); 201 }else{ 202 iter.remove(); 203 } 204 } 205 206 if(!entries.isEmpty()){ 207 LOGGER.debug("Removing invalid URLs."); 208 List<String> photoGUIDs = new ArrayList<>(); 209 List<String> videoGUIDs = new ArrayList<>(); 210 for(URLEntry e : entries){ 211 switch(e.getMediaType()){ 212 case PHOTO: 213 photoGUIDs.add(e.getGUID()); 214 break; 215 case VIDEO: 216 videoGUIDs.remove(e.getGUID()); 217 break; 218 default: 219 throw new UnsupportedOperationException("Unhandeled media type: "+e.getMediaType().name()); 220 } 221 } 222 223 if(photoGUIDs.isEmpty()){ 224 LOGGER.debug("No photos to remove."); 225 }else{ 226 ServiceInitializer.getDAOHandler().getSolrDAO(PhotoDAO.class).remove(photoGUIDs); 227 urlContentDAO.removeEntries(photoGUIDs); 228 CAContentCore.scheduleTask( 229 (new FeedbackTaskBuilder(TaskType.FEEDBACK)) 230 .setBackends(getBackends()) 231 .addDeletedPhotos(photoGUIDs) 232 .build() 233 ); 234 } 235 236 if(videoGUIDs.isEmpty()){ 237 LOGGER.debug("No videos to remove."); 238 }else{ 239 ServiceInitializer.getDAOHandler().getSolrDAO(VideoDAO.class).remove(videoGUIDs); 240 urlContentDAO.removeEntries(videoGUIDs); 241 VideoContentCore.scheduleTask( 242 (new VideoFeedbackTask.FeedbackTaskBuilder(TaskType.FEEDBACK)) 243 .setBackends(getBackends()) 244 .addDeletedVideos(videoGUIDs) 245 .build() 246 ); 247 } 248 } 249 }finally{ 250 USER_IDENTITY_LOCK.release(userId); 251 } 252 return true; 253 } 254 255 /** 256 * helper method for adding photo URLs 257 * 258 * @param userId 259 * @param urls list of URLs, validity will NOT be checked 260 * @return the generated task details 261 * @see #addUrls(core.tut.pori.utils.MediaUrlValidator.MediaType, UserIdentity, Collection) 262 */ 263 private PhotoTaskDetails addPhotoUrls(UserIdentity userId, Collection<String> urls) { 264 USER_IDENTITY_LOCK.acquire(userId); 265 PhotoList forAnalysis = new PhotoList(); 266 try{ 267 for(String url : urls){ 268 Photo photo = new Photo(null, userId, SERVICE_TYPE, Visibility.PUBLIC); 269 photo.setUrl(url); 270 forAnalysis.addPhoto(photo); 271 } 272 273 PhotoDAO photoDAO = ServiceInitializer.getDAOHandler().getSolrDAO(PhotoDAO.class); 274 URLContentDAO urlContentDAO = ServiceInitializer.getDAOHandler().getSQLDAO(URLContentDAO.class); 275 List<URLEntry> entries = urlContentDAO.getEntries(null, EnumSet.of(MediaType.PHOTO), urls, userId); 276 if(entries == null){ // no previously known URLs 277 photoDAO.insert(forAnalysis); // this will generate GUIDs 278 for(Photo p : forAnalysis.getPhotos()){ 279 urlContentDAO.addEntry(new URLEntry(p.getGUID(), MediaType.PHOTO, p.getUrl(), userId)); 280 } 281 }else{ // some or all of the URLs are known 282 for(Photo p : forAnalysis.getPhotos()){ 283 String url = p.getUrl(); 284 String guid = findGUID(entries, url); 285 if(guid == null){ // URL not known previously 286 photoDAO.insert(p); // this will generate GUID 287 urlContentDAO.addEntry(new URLEntry(p.getGUID(), MediaType.PHOTO, url, userId)); 288 }else{ 289 p.setGUID(guid); 290 } 291 } // for 292 } 293 } finally { 294 USER_IDENTITY_LOCK.release(userId); 295 } 296 297 LOGGER.debug("Creating a new analysis task..."); 298 PhotoTaskDetails details = new PhotoTaskDetails(TaskType.ANALYSIS); 299 details.setUserId(userId); 300 details.setBackends(getBackends(getBackendCapabilities())); 301 details.setPhotoList(forAnalysis); 302 details.setTaskParameters(ANALYSIS_PARAMETERS_PHOTO); 303 304 if(isAutoSchedule()){ 305 LOGGER.debug("Scheduling photo analysis task."); 306 CAContentCore.scheduleTask(details); 307 }else{ 308 LOGGER.debug("Auto-schedule is disabled."); 309 } 310 311 return details; 312 } 313 314 /** 315 * helper method for finding GUID matching with the given URL from the given list of entries 316 * 317 * If there are multiple matches, this will return the first match (first depending on the iteration order of the passed collection) 318 * 319 * @param entries 320 * @param url 321 * @return GUID or null if not found 322 */ 323 private String findGUID(Collection<URLEntry> entries, String url){ 324 if(entries == null){ 325 LOGGER.warn("Null entries."); 326 return null; 327 } 328 329 for(URLEntry e : entries){ 330 if(url.equals(e.getUrl())){ 331 return e.getGUID(); 332 } 333 } 334 return url; 335 } 336 337 /** 338 * helper method for adding video URLs 339 * 340 * @param userId 341 * @param urls 342 * @return the generated task details or null if no valid content 343 * @see #addUrls(core.tut.pori.utils.MediaUrlValidator.MediaType, UserIdentity, Collection) 344 */ 345 private VideoTaskDetails addVideoUrls(UserIdentity userId, Collection<String> urls) { 346 USER_IDENTITY_LOCK.acquire(userId); 347 VideoList forAnalysis = new VideoList(); 348 try{ 349 for(String url : urls){ 350 Video video = new Video(null, userId, SERVICE_TYPE, Visibility.PUBLIC); 351 video.setUrl(url); 352 forAnalysis.addVideo(video); 353 } 354 355 VideoDAO videoDAO = ServiceInitializer.getDAOHandler().getSolrDAO(VideoDAO.class); 356 URLContentDAO urlContentDAO = ServiceInitializer.getDAOHandler().getSQLDAO(URLContentDAO.class); 357 List<URLEntry> entries = urlContentDAO.getEntries(null, EnumSet.of(MediaType.VIDEO), urls, userId); 358 if(entries == null){ // no previously known urls 359 videoDAO.insert(forAnalysis); // this will generate guids 360 for(Video v : forAnalysis.getVideos()){ 361 urlContentDAO.addEntry(new URLEntry(v.getGUID(), MediaType.VIDEO, v.getUrl(), userId)); 362 } 363 }else{ // some or all of the urls are known 364 for(Video v : forAnalysis.getVideos()){ 365 String url = v.getUrl(); 366 String guid = findGUID(entries, url); 367 if(guid == null){ // url not known previously 368 videoDAO.insert(v); // this will generate guid 369 urlContentDAO.addEntry(new URLEntry(v.getGUID(), MediaType.VIDEO, url, userId)); 370 }else{ 371 v.setGUID(guid); 372 } 373 } // for 374 } 375 } finally { 376 USER_IDENTITY_LOCK.release(userId); 377 } 378 379 LOGGER.debug("Creating a new analysis task..."); 380 VideoTaskDetails details = new VideoTaskDetails(TaskType.ANALYSIS); 381 details.setUserId(userId); 382 details.setBackends(getBackends(getBackendCapabilities())); 383 details.setVideoList(forAnalysis); 384 details.setTaskParameters(ANALYSIS_PARAMETERS_VIDEO); 385 386 if(isAutoSchedule()){ 387 LOGGER.debug("Scheduling video analysis task."); 388 VideoContentCore.scheduleTask(details); 389 }else{ 390 LOGGER.debug("Auto-schedule is disabled."); 391 } 392 393 return details; 394 } 395 396 /** 397 * Add the given list of URLs. 398 * 399 * If a given URL already exists in the database, it will not be re-added, but new analysis task for the url will be created and scheduled. 400 * 401 * Note that this method CANNOT be used to remove previously added (and valid) URLs, which are now invalid. The invalid URLs will simply be ignored. 402 * Use synchronize if you want to clear the database of invalid URLs. 403 * 404 * @param mediaType 405 * @param userId 406 * @param urls 407 * @throws IllegalArgumentException on bad input data 408 * @throws UnsupportedOperationException on unsuuported media type 409 * @see #synchronizeAccount(UserIdentity) 410 */ 411 public void addUrls(MediaType mediaType, UserIdentity userId, Collection<String> urls) throws IllegalArgumentException, UnsupportedOperationException{ 412 if(urls == null || urls.isEmpty()){ 413 LOGGER.warn("Empty url list."); 414 return; 415 } 416 417 AbstractTaskDetails details = null; 418 switch(mediaType){ 419 case PHOTO: 420 details = addPhotoUrls(userId, urls); 421 break; 422 case VIDEO: 423 details = addVideoUrls(userId, urls); 424 break; 425 default: 426 throw new UnsupportedOperationException("Unsupported media type: "+mediaType.name()); 427 } 428 429 if(details == null){ 430 throw new IllegalArgumentException("No valid content."); 431 } 432 433 notifyAnalysisTaskCreated(details); 434 } 435 436 /** 437 * A URL entry. 438 */ 439 public static class URLEntry { 440 private String _guid = null; 441 private MediaType _mediaType = null; 442 private String _url = null; 443 private UserIdentity _userId = null; 444 445 /** 446 * @param guid 447 * @param mediaType 448 * @param url 449 * @param userId 450 */ 451 public URLEntry(String guid, MediaType mediaType, String url, UserIdentity userId) { 452 super(); 453 _guid = guid; 454 _mediaType = mediaType; 455 _url = url; 456 _userId = userId; 457 } 458 459 /** 460 * 461 */ 462 protected URLEntry(){ 463 // nothing needed 464 } 465 466 /** 467 * @return the guid 468 */ 469 public String getGUID() { 470 return _guid; 471 } 472 473 /** 474 * @return the mediaType 475 */ 476 public MediaType getMediaType() { 477 return _mediaType; 478 } 479 480 /** 481 * @return the URL 482 */ 483 public String getUrl() { 484 return _url; 485 } 486 487 /** 488 * @return the userId 489 */ 490 public UserIdentity getUserId() { 491 return _userId; 492 } 493 494 /** 495 * @param guid the GUID to set 496 */ 497 protected void setGUID(String guid) { 498 _guid = guid; 499 } 500 501 /** 502 * @param mediaType the mediaType to set 503 */ 504 protected void setMediaType(MediaType mediaType) { 505 _mediaType = mediaType; 506 } 507 508 /** 509 * @param url the URL to set 510 */ 511 protected void setUrl(String url) { 512 _url = url; 513 } 514 515 /** 516 * @param userId the userId to set 517 */ 518 protected void setUserId(UserIdentity userId) { 519 _userId = userId; 520 } 521 } // class URLEntry 522}