Java tutorial
/* * Copyright 2012 buddycloud * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.buddycloud.mediaserver.business.dao; import java.awt.image.BufferedImage; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.text.DateFormat; import java.util.List; import java.util.Properties; import javax.imageio.ImageIO; import org.apache.commons.codec.digest.DigestUtils; import org.apache.commons.fileupload.FileItem; import org.apache.commons.fileupload.FileUploadException; import org.apache.commons.fileupload.disk.DiskFileItemFactory; import org.apache.commons.io.FileUtils; import org.apache.commons.io.IOUtils; import org.apache.commons.lang.RandomStringUtils; import org.restlet.Request; import org.restlet.data.Form; import org.restlet.engine.util.Base64; import org.restlet.ext.fileupload.RestletFileUpload; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.buddycloud.mediaserver.business.jdbc.MetaDataSource; import com.buddycloud.mediaserver.business.model.Media; import com.buddycloud.mediaserver.business.model.Preview; import com.buddycloud.mediaserver.business.util.AudioUtils; import com.buddycloud.mediaserver.business.util.ImageUtils; import com.buddycloud.mediaserver.business.util.MimeTypeMapping; import com.buddycloud.mediaserver.business.util.VideoUtils; import com.buddycloud.mediaserver.business.util.XMPPUtils; import com.buddycloud.mediaserver.commons.Constants; import com.buddycloud.mediaserver.commons.MediaServerConfiguration; import com.buddycloud.mediaserver.commons.Thumbnail; import com.buddycloud.mediaserver.commons.exception.InvalidPreviewFormatException; import com.buddycloud.mediaserver.commons.exception.MediaNotFoundException; import com.buddycloud.mediaserver.commons.exception.MetadataSourceException; import com.buddycloud.mediaserver.commons.exception.UserNotAllowedException; import com.buddycloud.mediaserver.xmpp.XMPPToolBox; import com.buddycloud.mediaserver.xmpp.pubsub.PubSubClient; import com.buddycloud.mediaserver.xmpp.pubsub.capabilities.MemberDecorator; import com.buddycloud.mediaserver.xmpp.pubsub.capabilities.ModeratorDecorator; import com.buddycloud.mediaserver.xmpp.pubsub.capabilities.OwnerDecorator; import com.buddycloud.mediaserver.xmpp.pubsub.capabilities.PublisherDecorator; import com.google.gson.Gson; import com.google.gson.GsonBuilder; /** * Provides a Data Access Object to metadata * database and media file system. * * @author Rodrigo Duarte Sousa - rodrigodsousa@gmail.com */ public class MediaDAO { private static Logger LOGGER = LoggerFactory.getLogger(MediaDAO.class); protected MetaDataSource dataSource; protected PubSubClient pubsub; protected Properties configuration; protected Gson gson; protected MediaDAO() { this.dataSource = new MetaDataSource(); this.pubsub = XMPPToolBox.getInstance().getPubSubClient(); this.gson = new GsonBuilder().setDateFormat(DateFormat.FULL, DateFormat.FULL).create(); this.configuration = MediaServerConfiguration.getInstance().getConfiguration(); } /** * Return media max-age to be used on cache headers * @return Cache max-age */ public int getMaxAge() { return Integer.valueOf(configuration.getProperty(MediaServerConfiguration.CACHE_MAX_AGE)); } /** * Deletes a media file ant its metadata. * @param userId the user that is trying to delete media. * @param entityId media channel's id. * @param mediaId media to be deleted. * @throws MetadataSourceException if something goes wrong while retrieving media's metadata. * @throws MediaNotFoundException there is no media with such id. * @throws UserNotAllowedException this {@link userId} is not allowed to perform this operation. */ public void deleteMedia(String userId, String entityId, String mediaId) throws MetadataSourceException, MediaNotFoundException, UserNotAllowedException { boolean isAvatar = isAvatar(mediaId); if (isAvatar) { mediaId = dataSource.getEntityAvatarId(entityId); } boolean isUploader = dataSource.getMediaUploader(mediaId).equals(userId); if (!isUploader) { boolean isUserAllowed = pubsub.matchUserCapability(userId, entityId, new OwnerDecorator(new ModeratorDecorator())); if (!isUserAllowed) { LOGGER.debug("User '" + userId + "' not allowed to peform delete operation on: " + mediaId); throw new UserNotAllowedException(userId); } } String fullDirectoryPath = getDirectory(entityId); File media = new File(fullDirectoryPath + File.separator + mediaId); if (!media.exists()) { throw new MediaNotFoundException(mediaId, entityId); } LOGGER.debug("Deleting media. Media ID: " + mediaId); if (isAvatar) { // delete avatars table entry dataSource.deleteEntityAvatar(entityId); } // delete existent previews from media deletePreviews(mediaId, fullDirectoryPath); // delete file and metadata media.delete(); dataSource.deleteMedia(mediaId); } protected void deletePreviews(String mediaId, String dirPath) throws MetadataSourceException { List<String> previews = dataSource.getPreviewsFromMedia(mediaId); if (!previews.isEmpty()) { for (String previewId : previews) { File preview = new File(dirPath + File.separator + previewId); preview.delete(); } dataSource.deletePreviewsFromMedia(mediaId); } } /** * Gets an information list from all medias in a channel. * @param userId the user that is trying to request the media lsit. * @param entityId media channel's id. * @throws MetadataSourceException if something goes wrong while retrieving media's metadata. * @throws UserNotAllowedException this {@link userId} is not allowed to perform this operation. */ public String getMediasInfo(String userId, String entityId, Integer max, String after) throws UserNotAllowedException, MetadataSourceException { if (userId != null) { boolean isUserAllowed = pubsub.matchUserCapability(userId, entityId, new OwnerDecorator(new ModeratorDecorator(new PublisherDecorator(new MemberDecorator())))); if (!isUserAllowed) { LOGGER.debug("User '" + userId + "' not allowed to peform get info operation on: " + entityId); throw new UserNotAllowedException(userId); } } LOGGER.debug("Getting medias info from: " + entityId); List<Media> medias = dataSource.getMediasInfo(entityId, max, after); return gson.toJson(medias); } /** * Gets a media file. * @param userId the user that is trying to get the media. * @param entityId media channel's id. * @param mediaId media to be fetched. * @return media file. * @throws MetadataSourceException if something goes wrong while retrieving media's metadata. * @throws MediaNotFoundException there is no media with such id. * @throws IOException if something goes wrong while getting media file. * @throws UserNotAllowedException this {@link userId} is not allowed to perform this operation. */ public File getMedia(String userId, String entityId, String mediaId) throws MetadataSourceException, MediaNotFoundException, IOException, UserNotAllowedException { if (isAvatar(mediaId)) { return getAvatar(entityId); } if (userId != null) { boolean isUserAllowed = pubsub.matchUserCapability(userId, entityId, new OwnerDecorator(new ModeratorDecorator(new PublisherDecorator(new MemberDecorator())))); if (!isUserAllowed) { LOGGER.debug("User '" + userId + "' not allowed to peform get operation on: " + entityId); throw new UserNotAllowedException(userId); } } LOGGER.debug("Getting media. Media ID: " + mediaId); String fullDirectoryPath = getDirectory(entityId); File media = new File(fullDirectoryPath + File.separator + mediaId); if (!media.exists()) { throw new MediaNotFoundException(mediaId, entityId); } return media; } /** * Gets a channel avatar. * @param entityId avatar's channel. * @return avatar file. * @throws MetadataSourceException if something goes wrong while retrieving avatar's metadata. * @throws MediaNotFoundException there is no media representing {@link entityId} avatar. */ public File getAvatar(String entityId) throws MetadataSourceException, MediaNotFoundException, IOException { String mediaId = dataSource.getEntityAvatarId(entityId); LOGGER.debug("Getting avatar. Entity ID: " + entityId); String fullDirectoryPath = getDirectory(entityId); File media = new File(fullDirectoryPath + File.separator + mediaId); if (!media.exists()) { throw new MediaNotFoundException(mediaId, entityId); } return media; } /** * Gets a media preview. * @param userId user that is requesting the preview. * @param entityId media's channel. * @param mediaId media to be fetched. * @param size preview size limit. * @return media's {@link Thumbnail} * @throws MetadataSourceException if something goes wrong while retrieving media's metadata. * @throws MediaNotFoundException there is no media with such id. * @throws IOException if something goes wrong while getting preview file. * @throws InvalidPreviewFormatException if the client is not requesting media from an image or video. * @throws UserNotAllowedException this {@link userId} is not allowed to perform this operation. */ public Thumbnail getMediaPreview(String userId, String entityId, String mediaId, Integer size) throws MetadataSourceException, MediaNotFoundException, IOException, InvalidPreviewFormatException, UserNotAllowedException { return getMediaPreview(userId, entityId, mediaId, size, size); } /** * Gets a media preview. * @param userId user that is requesting the preview. * @param entityId media's channel. * @param mediaId media to be fetched. * @param maxHeight preview height limit. * @param maxWidth preview width limit. * @return media's {@link Thumbnail} * @throws MetadataSourceException if something goes wrong while retrieving media's metadata. * @throws MediaNotFoundException there is no media with such id. * @throws IOException if something goes wrong while getting preview file. * @throws InvalidPreviewFormatException if the client is not requesting media from an image or video. * @throws UserNotAllowedException this {@link userId} is not allowed to perform this operation. */ public Thumbnail getMediaPreview(String userId, String entityId, String mediaId, Integer maxHeight, Integer maxWidth) throws MetadataSourceException, MediaNotFoundException, IOException, InvalidPreviewFormatException, UserNotAllowedException { if (isAvatar(mediaId)) { return getAvatarPreview(userId, entityId, maxHeight, maxWidth); } if (userId != null) { boolean isUserAllowed = pubsub.matchUserCapability(userId, entityId, new OwnerDecorator(new ModeratorDecorator(new PublisherDecorator(new MemberDecorator())))); if (!isUserAllowed) { LOGGER.debug("User '" + userId + "' not allowed to get media on: " + entityId); throw new UserNotAllowedException(userId); } } LOGGER.debug("Getting media preview. Media ID: " + mediaId); return getPreview(entityId, mediaId, maxHeight, maxWidth, getDirectory(entityId)); } private Thumbnail getAvatarPreview(String userId, String entityId, Integer maxHeight, Integer maxWidth) throws MetadataSourceException, MediaNotFoundException, IOException, InvalidPreviewFormatException { String mediaId = dataSource.getEntityAvatarId(entityId); LOGGER.debug("Getting avatar preview. Avatar ID: " + entityId); return getPreview(entityId, mediaId, maxHeight, maxWidth, getDirectory(entityId)); } /** * Gets a media MIME type. * @param entityId media's channel. * @param mediaId media's unique id. * @return media's MIME type. * @throws MetadataSourceException if something goes wrong while retrieving media's metadata. * @throws MediaNotFoundException if there is no media for the entity's avatar. */ public String getMediaType(String entityId, String mediaId) throws MetadataSourceException, MediaNotFoundException { if (isAvatar(mediaId)) { mediaId = dataSource.getEntityAvatarId(entityId); if (mediaId == null) { throw new MediaNotFoundException("avatar", entityId); } } return dataSource.getMediaMimeType(mediaId); } /** * Updates media's metadata. * @param userId the user that is requesting the update. * @param entityId media's channel. * @param mediaId media to be updated. * @param request contains which are the metadata to be updated. * @return media's new metadata. * @throws FileUploadException if something goes wrong during request parsing. * @throws MetadataSourceException if something goes wrong while retrieving media's metadata. * @throws MediaNotFoundException there is no media with such id. * @throws UserNotAllowedException the @{link userId} is not allowed to perform this operation. */ public String updateMedia(String userId, String entityId, String mediaId, Form form) throws MetadataSourceException, MediaNotFoundException, UserNotAllowedException { if (isAvatar(mediaId)) { mediaId = dataSource.getEntityAvatarId(entityId); } boolean isUploader = dataSource.getMediaUploader(mediaId).equals(userId); boolean isMember = pubsub.matchUserCapability(userId, entityId, new MemberDecorator()); if (!(isUploader && isMember)) { boolean isUserAllowed = pubsub.matchUserCapability(userId, entityId, new OwnerDecorator(new ModeratorDecorator())); if (!isUserAllowed) { LOGGER.debug("User '" + userId + "' not allowed to peform delete operation on: " + mediaId); throw new UserNotAllowedException(userId); } } Media media = dataSource.getMedia(mediaId); if (media == null) { throw new MediaNotFoundException(mediaId, entityId); } // get form fields String fileName = form.getFirstValue(Constants.NAME_FIELD); if (fileName != null) { media.setFileName(fileName); } String title = form.getFirstValue(Constants.TITLE_FIELD); if (title != null) { media.setTitle(title); } String description = form.getFirstValue(Constants.DESC_FIELD); if (description != null) { media.setDescription(description); } dataSource.updateMediaFields(media); // Update last updated date dataSource.updateMediaLastUpdated(mediaId); LOGGER.debug("Media sucessfully updated. Media ID: " + media.getId()); return gson.toJson(media); } /** * Uploads media from web form. * @param userId user that is uploading the media. * @param entityId channel where the media will belong. * @param form web form containing the media. * @param isAvatar if the media to be uploaded is an avatar. * @return media's metadata, if the upload ends with success * @throws FileUploadException the is something wrong with the request. * @throws UserNotAllowedException the user {@link userId} is now allowed to upload media in this channel. */ public String insertWebFormMedia(String userId, String entityId, Form form, boolean isAvatar) throws FileUploadException, UserNotAllowedException { LOGGER.debug("User '" + userId + "' trying to upload web-form media on: " + entityId); boolean isUserAllowed = pubsub.matchUserCapability(userId, entityId, new OwnerDecorator(new ModeratorDecorator(new PublisherDecorator()))); if (!isUserAllowed) { LOGGER.debug("User '" + userId + "' not allowed to uploade file on: " + entityId); throw new UserNotAllowedException(userId); } // get form fields String fileName = form.getFirstValue(Constants.NAME_FIELD); String title = form.getFirstValue(Constants.TITLE_FIELD); String description = form.getFirstValue(Constants.DESC_FIELD); String data = form.getFirstValue(Constants.DATA_FIELD); if (data == null) { throw new FileUploadException("Must provide the file data."); } byte[] dataArray = Base64.decode(data); String contentType = form.getFirstValue(Constants.TYPE_FIELD); if (contentType == null) { throw new FileUploadException("Must provide a " + Constants.TYPE_FIELD + " for the uploaded file."); } // storing Media media = storeMedia(fileName, title, description, XMPPUtils.getBareId(userId), entityId, contentType, dataArray, isAvatar); LOGGER.debug("Media sucessfully added. Media ID: " + media.getId()); return gson.toJson(media); } /** * Uploads media. * @param userId user that is uploading the media. * @param entityId channel where the media will belong. * @param request media file and required upload fields. * @param isAvatar if the media to be uploaded is an avatar. * @return media's metadata, if the upload ends with success * @throws FileUploadException the is something wrong with the request. * @throws UserNotAllowedException the user {@link userId} is now allowed to upload media in this channel. */ public String insertFormDataMedia(String userId, String entityId, Request request, boolean isAvatar) throws FileUploadException, UserNotAllowedException { LOGGER.debug("User '" + userId + "' trying to upload form-data media in: " + entityId); boolean isUserAllowed = pubsub.matchUserCapability(userId, entityId, new OwnerDecorator(new ModeratorDecorator(new PublisherDecorator()))); if (!isUserAllowed) { LOGGER.debug("User '" + userId + "' not allowed to upload file in: " + entityId); throw new UserNotAllowedException(userId); } DiskFileItemFactory factory = new DiskFileItemFactory(); factory.setSizeThreshold( Integer.valueOf(configuration.getProperty(MediaServerConfiguration.MEDIA_SIZE_LIMIT_PROPERTY))); List<FileItem> items = null; try { RestletFileUpload upload = new RestletFileUpload(factory); items = upload.parseRequest(request); } catch (Throwable e) { throw new FileUploadException("Invalid request data."); } // get form fields String fileName = getFormFieldContent(items, Constants.NAME_FIELD); String title = getFormFieldContent(items, Constants.TITLE_FIELD); String description = getFormFieldContent(items, Constants.DESC_FIELD); String contentType = getFormFieldContent(items, Constants.TYPE_FIELD); FileItem fileField = getFileFormField(items); if (fileField == null) { throw new FileUploadException("Must provide the file data."); } byte[] dataArray = fileField.get(); if (contentType == null) { if (fileField.getContentType() != null) { contentType = fileField.getContentType(); } else { throw new FileUploadException("Must provide a " + Constants.TYPE_FIELD + " for the uploaded file."); } } // storing Media media = storeMedia(fileName, title, description, XMPPUtils.getBareId(userId), entityId, contentType, dataArray, isAvatar); LOGGER.debug("Media sucessfully added. Media ID: " + media.getId()); return gson.toJson(media); } protected Media storeMedia(String fileName, String title, String description, String author, String entityId, String mimeType, byte[] data, final boolean isAvatar) throws FileUploadException { String directory = getDirectory(entityId); mkdir(directory); // TODO assert id uniqueness String mediaId = RandomStringUtils.randomAlphanumeric(20); String filePath = directory + File.separator + mediaId; File file = new File(filePath); LOGGER.debug("Storing new media: " + file.getAbsolutePath()); try { FileOutputStream out = FileUtils.openOutputStream(file); out.write(data); out.close(); } catch (IOException e) { LOGGER.error("Error while storing file: " + filePath, e); throw new FileUploadException(e.getMessage()); } final Media media = createMedia(mediaId, fileName, title, description, author, entityId, mimeType, file, isAvatar); // store media's metadata new Thread() { public void start() { try { dataSource.storeMedia(media); if (isAvatar) { if (dataSource.getEntityAvatarId(media.getEntityId()) != null) { dataSource.updateEntityAvatar(media.getEntityId(), media.getId()); } else { dataSource.storeAvatar(media); } } } catch (MetadataSourceException e) { // do nothing LOGGER.error("Database error", e); } } }.start(); return media; } protected Thumbnail getPreview(String entityId, String mediaId, Integer maxHeight, Integer maxWidth, String mediaDirectory) throws MetadataSourceException, IOException, InvalidPreviewFormatException, MediaNotFoundException { File media = new File(mediaDirectory + File.separator + mediaId); if (!media.exists()) { throw new MediaNotFoundException(mediaId, entityId); } String previewId = dataSource.getPreviewId(mediaId, maxHeight, maxWidth); if (previewId != null) { File preview = new File(mediaDirectory + File.separator + previewId); if (!preview.exists()) { dataSource.deletePreview(previewId); } else { return new Thumbnail(dataSource.getPreviewMimeType(previewId), IOUtils.toByteArray(FileUtils.openInputStream(preview))); } } else { // generate random id previewId = RandomStringUtils.randomAlphanumeric(20); } return buildNewPreview(media, mediaId, previewId, mediaDirectory, maxHeight, maxWidth); } private Thumbnail buildNewPreview(File media, String mediaId, String previewId, String mediaDirectory, Integer maxHeight, Integer maxWidth) throws MetadataSourceException, IOException, InvalidPreviewFormatException { String extension = dataSource.getMediaExtension(mediaId); BufferedImage previewImg = null; Thumbnail thumbnail = null; if (ImageUtils.isImage(extension)) { previewImg = ImageUtils.createImagePreview(media, maxWidth, maxHeight); thumbnail = new Thumbnail(dataSource.getMediaMimeType(mediaId), ImageUtils.imageToBytes(previewImg, extension)); } else if (VideoUtils.isVideo(extension)) { previewImg = new VideoUtils(media).createVideoPreview(maxWidth, maxHeight); thumbnail = new Thumbnail(VideoUtils.PREVIEW_MIME_TYPE, ImageUtils.imageToBytes(previewImg, VideoUtils.PREVIEW_TYPE)); } else { throw new InvalidPreviewFormatException(extension); } // store preview in another flow new StorePreviewThread(previewId, mediaDirectory, mediaId, thumbnail.getMimeType(), maxHeight, maxWidth, extension, previewImg).start(); return thumbnail; } public boolean isAvatar(String mediaId) { return mediaId.equals(Constants.AVATAR_ARG); } protected String getFormFieldContent(List<FileItem> items, String fieldName) { String field = null; for (int i = 0; i < items.size(); i++) { FileItem item = items.get(i); if (fieldName.equals(item.getFieldName().toLowerCase())) { field = item.getString(); items.remove(i); break; } } return field; } protected FileItem getFileFormField(List<FileItem> items) { FileItem field = null; for (int i = 0; i < items.size(); i++) { FileItem item = items.get(i); if (Constants.DATA_FIELD.equals(item.getFieldName().toLowerCase())) { field = item; break; } } return field; } protected Media createMedia(String mediaId, String fileName, String title, String description, String author, String entityId, String mimeType, File file, boolean isAvatar) { Media media = new Media(); media.setId(mediaId); media.setFileName(fileName); media.setEntityId(entityId); media.setAuthor(author); media.setDescription(description); media.setTitle(title); media.setMimeType(mimeType); String fileExtension = getFileExtension(fileName, mimeType); media.setFileExtension(fileExtension); try { if (ImageUtils.isImage(fileExtension)) { BufferedImage img = ImageIO.read(file); if (isAvatar && !ImageUtils.isSquare(img)) { img = ImageUtils.cropMaximumSquare(img); // update image file file = ImageUtils.storeImageIntoFile(img, fileExtension, file.getAbsolutePath()); } media.setHeight(img.getHeight()); media.setWidth(img.getWidth()); } else if (VideoUtils.isVideo(fileExtension)) { VideoUtils videoUtils = new VideoUtils(file); media.setLength(videoUtils.getVideoLength()); media.setHeight(videoUtils.getVideoHeight()); media.setWidth(videoUtils.getVideoWidth()); } else if (AudioUtils.isAudio(fileExtension)) { media.setLength(AudioUtils.getAudioLength(file)); } } catch (Throwable t) { LOGGER.error("Error while resolving media format properties", t); } // set after possible cropping media.setFileSize(file.length()); media.setShaChecksum(getFileShaChecksum(file)); return media; } protected String getFileExtension(String fileName, String mimeType) { if (fileName != null) { String[] dotSplit = fileName.split("."); if (dotSplit.length > 1) { return dotSplit[dotSplit.length - 1]; } } return MimeTypeMapping.lookupExtension(mimeType); } protected Preview createPreview(String previewId, String mediaId, String mimeType, Integer height, Integer width, File file) { Preview preview = new Preview(); preview.setFileSize(file.length()); preview.setHeight(height); preview.setWidth(width); preview.setId(previewId); preview.setMediaId(mediaId); preview.setShaChecksum(getFileShaChecksum(file)); preview.setMimeType(mimeType); return preview; } protected String getFileShaChecksum(File file) { try { return DigestUtils.shaHex(FileUtils.openInputStream(file)); } catch (IOException e) { LOGGER.error("Error during media SHA1 checksum generation.", e); } return null; } protected boolean mkdir(String fullDirectoryPath) { File directory = new File(fullDirectoryPath); return directory.mkdir(); } private String getDirectory(String entityId) { return configuration.getProperty(MediaServerConfiguration.MEDIA_STORAGE_ROOT_PROPERTY) + File.separator + entityId; } // Thread responsible to store preview's file and metadata private class StorePreviewThread extends Thread { private String previewId; private String mediaId; private Integer height; private Integer width; private String directory; private String extension; private String mimeType; private BufferedImage img; StorePreviewThread(String previewId, String directory, String mediaId, String mimeType, Integer height, Integer width, String extension, BufferedImage img) { this.previewId = previewId; this.directory = directory; this.mediaId = mediaId; this.height = height; this.width = width; this.extension = extension; this.mimeType = mimeType; this.img = img; } public void start() { try { File previewFile = ImageUtils.storeImageIntoFile(img, extension, directory + File.separator + previewId); Preview preview = createPreview(previewId, mediaId, mimeType, height, width, previewFile); dataSource.storePreview(preview); } catch (Exception e) { LOGGER.error("Error while storing preview: " + previewId + ". Media ID: " + mediaId, e); } } } }