001/** 002 * Copyright 2011-2014 The Kuali Foundation Licensed under the 003 * Educational Community License, Version 2.0 (the "License"); you may 004 * not use this file except in compliance with the License. You may 005 * obtain a copy of the License at 006 * 007 * http://www.osedu.org/licenses/ECL-2.0 008 * 009 * Unless required by applicable law or agreed to in writing, 010 * software distributed under the License is distributed on an "AS IS" 011 * BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 012 * or implied. See the License for the specific language governing 013 * permissions and limitations under the License. 014 */ 015 016package org.kuali.mobility.writer.service; 017 018import net.coobird.thumbnailator.Thumbnails; 019import org.apache.commons.io.IOUtils; 020import org.slf4j.Logger; 021import org.slf4j.LoggerFactory; 022import org.kuali.mobility.writer.dao.*; 023import org.kuali.mobility.writer.entity.*; 024import org.springframework.beans.factory.annotation.Autowired; 025import org.springframework.transaction.annotation.Transactional; 026import org.springframework.web.multipart.MultipartFile; 027 028import javax.imageio.ImageIO; 029import java.awt.image.BufferedImage; 030import java.io.*; 031import java.util.List; 032 033 034/** 035 * Implementation of the WriterService. 036 * @author Kuali Mobility Team (mobility.collab@kuali.org) 037 * @since 3.0.0 038 */ 039public class WriterServiceImpl implements WriterService { 040 041 /** Maximum size an image should be stored as */ 042 private static final int MAX_IMAGE_HEIGHT = 1080; 043 044 /** Maximum size an image should be stored as */ 045 private static final int MAX_IMAGE_WIDTH = 1080; 046 047 /** Size for thumbnailed images*/ 048 private static final int MAX_IMAGE_THUMB_HEIGHT = 80; 049 050 /** Size for thumbnailed images*/ 051 private static final int MAX_IMAGE_THUMB_WIDTH = 80; 052 053 private static final float IMAGE_QUALITY = 0.9f; 054 055 056 /** A reference to a logger */ 057 private static final Logger LOG = LoggerFactory.getLogger(WriterServiceImpl.class); 058 /** 059 * Data Access Object for Articles. 060 */ 061 @Autowired 062 private ArticleDao articleDao; 063 064 /** 065 * Data Access Object for categogies. 066 */ 067 @Autowired 068 private CategoryDao categoryDao; 069 070 /** 071 * Data Access Object for Comments. 072 */ 073 @Autowired 074 private CommentDao commentDao; 075 076 /** 077 * Data Access Object for Media. 078 */ 079 @Autowired 080 private MediaDao mediaDao; 081 082 /** 083 * Data Access Object for Topics. 084 */ 085 @Autowired 086 private TopicDao topicDao; 087 /** 088 * Data Access Object for Article Rejections. 089 */ 090 @Autowired 091 private ArticleRejectionDao articleRejectionDao; 092 093 094// /** 095// * A reference to the user notification service 096// TODO 097// @Autowired 098// private UserPushService userPushService; 099// */ 100 101 102 /** 103 * Instantiates a new Writer service impl. 104 */ 105 public WriterServiceImpl(){ 106 } 107 108 109 @Override 110 //@Cacheable(value="wapadArticle", key="#articleId") 111 public Article getArticle(long articleId) { 112 return articleDao.getArticle(articleId); 113 } 114 115 116 @Override 117 public long getNumberSavedArticles(String instance, String userId, boolean isEditor) { 118 return this.articleDao.getNumberSavedArticles(instance, userId, isEditor); 119 } 120 121 @Override 122 public long getNumberRejectedArticles(String tool, String userId) { 123 return this.articleDao.getNumberRejectedArticles(tool, userId); 124 } 125 126 @Override 127 public long getNumberSubmittedArticles(String instance) { 128 return this.articleDao.getNumberSubmittedArticles(instance); 129 } 130 131 @Override 132 public List<Article> getSavedArticles(String tool, String userId, boolean isEditor) { 133 return articleDao.getSavedArticles(tool, userId, isEditor); 134 } 135 136 @Override 137 public List<Article> getRejectedArticles(String tool, String userId) { 138 return articleDao.getRejectedArticles(tool, userId); 139 } 140 141 @Override 142 public List<Article> getSubmittedArticles(String tool) { 143 return articleDao.getSubmittedArticles(tool); 144 } 145 146 @Override 147 public Comment addComment(Comment comment) { 148 return commentDao.addComment(comment); 149 } 150 151 @Override 152 @Transactional 153 //@CacheEvict(value="wapadArticle", key="#article.id") 154 public Article maintainArticle(Article article) { 155 return articleDao.maintainArticle(article); 156 } 157 158 @Override 159 public List<Comment> getCommentsForArticle(long articleId) { 160 return this.commentDao.getCommentsForArticle(articleId); 161 } 162 163 @Override 164 public int getNumberCommentForArticle(long articleId) { 165 return this.getCommentsForArticle(articleId).size(); 166 } 167 168 @Override 169 public ArticleRejection getArticleRejection(long rejectionId) { 170 return articleDao.getArticleRejection(rejectionId); 171 } 172 173 @Override 174 public Media getMedia(long mediaId) { 175 return mediaDao.getMedia(mediaId); 176 } 177 178 @Override 179 public String storeMedia(int mediaType, String extention, boolean isThumbnail, InputStream inputStream){ 180 return mediaDao.storeMedia(mediaType, extention, isThumbnail, inputStream); 181 } 182 183 @Override 184 public Media maintainMedia(Media media) { 185 return this.mediaDao.maintainMedia(media); 186 } 187 188 @Override 189 public void persistArticleRejection(ArticleRejection articleRejection) { 190 articleRejectionDao.persistArticleRejection(articleRejection); 191 } 192 193 @Override 194 //@Cacheable("wapadTopic") 195 public List<Topic> getTopics() { 196 return topicDao.getTopics(); 197 } 198 199 @Override 200 //@Cacheable(value="wapadTopic", key="#topicId") 201 public Topic getTopic(long topicId) { 202 return topicDao.getTopic(topicId); 203 } 204 205 @Override 206 public Topic saveTopic(Topic topic) { 207 return this.topicDao.saveTopic(topic); 208 } 209 210 @Override 211 public void deleteComment(long commentId) { 212 this.commentDao.deleteComment(commentId); 213 } 214 215 public Article updateMedia(Article article, Media media){ 216 217 // First remove old if there was 218 article = this.removeMedia(article, media.getType()); 219 if (media.getType() == Media.MEDIA_TYPE_VIDEO){ 220 article.setVideo(media); 221 } 222 else if(media.getType() == Media.MEDIA_TYPE_IMAGE){ 223 article.setImage(media); 224 } 225 article = this.maintainArticle(article); 226 return article; 227 } 228 229 public Article removeMedia(Article article, int mediaType){ 230 Media oldMedia = null; 231 if (mediaType == Media.MEDIA_TYPE_VIDEO){ 232 oldMedia = article.getVideo(); 233 article.setVideo(null); 234 }else if(mediaType == Media.MEDIA_TYPE_IMAGE){ 235 oldMedia = article.getImage(); 236 article.setImage(null); 237 } 238 239 /* 240 * If there was old media we need to persist 241 */ 242 if (oldMedia != null){ 243 article = this.maintainArticle(article); 244 mediaDao.removeMedia(oldMedia.getId()); 245 } 246 return article; 247 } 248 249 public Media uploadMediaData(MultipartFile mediaFile, int mediaType){ 250 // TODO this whole media saving code should be moved to a media store project! 251 Media returnMedia = null; 252 if (mediaFile!= null && !mediaFile.isEmpty()){ 253 try{ 254 String mediaExt = null; 255 // Get the file extension from the original filename 256 if (mediaFile.getOriginalFilename() != null){ 257 String oName = mediaFile.getOriginalFilename(); 258 int index = oName.lastIndexOf('.'); 259 if (index > 0){ 260 mediaExt = oName.substring(index+1); 261 } 262 } 263 // If the extension is still empty, we fallback to .dat 264 if (mediaExt == null || mediaExt.length() == 0){ 265 mediaExt = ".dat"; 266 } 267 268 InputStream originalMediaStream = mediaFile.getInputStream(); 269 Media uploadedMedia = new Media(); 270 uploadedMedia.setType(mediaType); 271 IOUtils.closeQuietly(originalMediaStream); 272 273 /*/ 274 * Attempt to get the mime type of the media 275 */ 276 if (mediaFile.getContentType() == null || mediaFile.getContentType().length() == 0){ 277 if (mediaType == Media.MEDIA_TYPE_VIDEO){ 278 uploadedMedia.setMimeType("video/3gp"); // Fallback format TODO get proper mapping 279 } 280 }else { 281 uploadedMedia.setMimeType(mediaFile.getContentType()); 282 } 283 if (mediaType == Media.MEDIA_TYPE_VIDEO){ 284 String originalPath = this.storeMedia(mediaType,mediaExt, false, originalMediaStream); 285 uploadedMedia.setPath(originalPath); 286 uploadedMedia.setThumbNailPath(""); // No thumbnail for video for now 287 } 288 else if (mediaType == Media.MEDIA_TYPE_IMAGE){ 289 uploadedMedia.setMimeType("image/jpeg"); // We allways save images a jpeg 290 291 InputStream scaledImageStream = null; 292 String newFilePath = null; 293 BufferedImage originalImage = ImageIO.read(mediaFile.getInputStream()); 294 if (originalImage == null){ 295 LOG.error("Failed to read uploaded image - posibly incorrect file type"); 296 return null; 297 } 298 299 // If we are saving an image, we need to resize the original to the max configured size, and also convert to jpeg 300 if (originalImage.getWidth() > MAX_IMAGE_WIDTH || originalImage.getHeight() > MAX_IMAGE_HEIGHT){ 301 scaledImageStream = this.resizeImage(mediaFile.getInputStream(), MAX_IMAGE_WIDTH, MAX_IMAGE_HEIGHT); 302 } 303 // Image is small enough, just convert to jpeg 304 else { 305 scaledImageStream = this.convertToJPEG(mediaFile.getInputStream()); 306 } 307 if (scaledImageStream == null){ 308 LOG.error("Failed to scale uploaded image - posibly incorrect file type"); 309 return null; 310 } 311 newFilePath = this.storeMedia(Media.MEDIA_TYPE_IMAGE,".jpeg", false, scaledImageStream); 312 uploadedMedia.setPath(newFilePath); 313 IOUtils.closeQuietly(scaledImageStream); 314 315 316 // Create a thumbnail of the image 317 scaledImageStream = this.resizeImage(mediaFile.getInputStream(), MAX_IMAGE_THUMB_WIDTH, MAX_IMAGE_THUMB_HEIGHT); 318 if (scaledImageStream == null){ 319 LOG.error("Failed to scale thumbnail of uploaded image - posibly incorrect file type"); 320 return null; 321 } 322 newFilePath = this.storeMedia(Media.MEDIA_TYPE_IMAGE,".jpeg", true, scaledImageStream); 323 uploadedMedia.setThumbNailPath(newFilePath); 324 IOUtils.closeQuietly(scaledImageStream); 325 } 326 returnMedia = this.maintainMedia(uploadedMedia); 327 }catch(IOException e){ 328 LOG.error("Exception trying to upload media data", e); 329 } 330 } 331 return returnMedia; 332 } 333 334 /** 335 * Resize the image to an appropriate thumbnail size. 336 * @param inputStream 337 * @return 338 * @throws java.io.IOException 339 */ 340 private final InputStream convertToJPEG(InputStream inputStream) 341 throws IOException{ 342 ByteArrayOutputStream os = new ByteArrayOutputStream(); 343 Thumbnails.of(inputStream) 344 .scale(1) 345 .outputQuality(IMAGE_QUALITY) 346 .outputFormat("jpeg") 347 .toOutputStream(os); 348 InputStream resizedStream = new ByteArrayInputStream(os.toByteArray()); 349 return resizedStream; 350 } 351 352 /** 353 * Resize the image to an appropriate thumbnail size. 354 * @param inputStream 355 * @return 356 * @throws java.io.IOException 357 */ 358 private final InputStream resizeImage(InputStream inputStream, int width, int height) 359 throws IOException{ 360 ByteArrayOutputStream os = new ByteArrayOutputStream(); 361 Thumbnails.of(inputStream) 362 .size(width, height) 363 .keepAspectRatio(true) 364 .outputQuality(IMAGE_QUALITY) 365 .outputFormat("jpeg") 366 .toOutputStream(os); 367 InputStream resizedStream = new ByteArrayInputStream(os.toByteArray()); 368 return resizedStream; 369 } 370 371 372 @Override 373 public long getNumArticles(String tool, long topicId) { 374 return this.articleDao.getNumArticles(tool, topicId); 375 } 376 377 @Override 378 public List<Article> getArticles(String tool, long topicId, int from, int fetchSize) { 379 return this.articleDao.getArticles(tool, topicId, from, fetchSize); 380 } 381 382 @Override 383 public void removeNotifications(String username) { 384 // TODO this.userPushService.deletePushesFor(username, "wapad"); 385 } 386 387 @Override 388 public List<Article> searchArticles(String tool, String searchText, int from, int fetchSize) { 389 return this.articleDao.searchArticles(tool, searchText, from, fetchSize); 390 } 391 392 @Override 393 public long searchArticlesCount(String tool, String searchText) { 394 return this.articleDao.searchArticlesCount(tool, searchText); 395 } 396 397 @Override 398 public File getMediaFile(long mediaId, boolean isThumbnail) throws FileNotFoundException { 399 return mediaDao.getMedia(mediaId, isThumbnail); 400 } 401 402 @Override 403 //@Cacheable("wapadCategory") 404 public List<Category> getCategories() { 405 return categoryDao.getCategories(); 406 } 407 408 @Override 409 //@Cacheable(value="wapadCategory", key="#categoryId") 410 public Category getCategory(long categoryId) { 411 return categoryDao.getCategory(categoryId); 412 } 413 414 @Override 415 public Category saveCategory(Category category) { 416 return this.categoryDao.saveCategory(category); 417 } 418 419}