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}