/*
This file is part of Cyclos (www.cyclos.org).
A project of the Social Trade Organisation (www.socialtrade.org).
Cyclos is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
Cyclos is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Cyclos; if not, write to the Free Software
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
package nl.strohalm.cyclos.services.customization;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.Arrays;
import java.util.Calendar;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import javax.imageio.ImageIO;
import javax.servlet.ServletContext;
import nl.strohalm.cyclos.dao.customizations.ImageDAO;
import nl.strohalm.cyclos.entities.Entity;
import nl.strohalm.cyclos.entities.Relationship;
import nl.strohalm.cyclos.entities.ads.Ad;
import nl.strohalm.cyclos.entities.customization.files.CustomizedFile;
import nl.strohalm.cyclos.entities.customization.files.CustomizedFileQuery;
import nl.strohalm.cyclos.entities.customization.images.AdImage;
import nl.strohalm.cyclos.entities.customization.images.CustomImage;
import nl.strohalm.cyclos.entities.customization.images.Image;
import nl.strohalm.cyclos.entities.customization.images.Image.Nature;
import nl.strohalm.cyclos.entities.customization.images.ImageCaptionDTO;
import nl.strohalm.cyclos.entities.customization.images.ImageDetailsDTO;
import nl.strohalm.cyclos.entities.customization.images.MemberImage;
import nl.strohalm.cyclos.entities.customization.images.OwneredImage;
import nl.strohalm.cyclos.entities.customization.images.StyleImage;
import nl.strohalm.cyclos.entities.customization.images.SystemImage;
import nl.strohalm.cyclos.entities.exceptions.EntityNotFoundException;
import nl.strohalm.cyclos.entities.members.Element;
import nl.strohalm.cyclos.entities.members.Member;
import nl.strohalm.cyclos.entities.settings.LocalSettings;
import nl.strohalm.cyclos.exceptions.PermissionDeniedException;
import nl.strohalm.cyclos.services.InitializingService;
import nl.strohalm.cyclos.services.customization.exceptions.ImageException;
import nl.strohalm.cyclos.services.fetch.FetchServiceLocal;
import nl.strohalm.cyclos.services.settings.SettingsServiceLocal;
import nl.strohalm.cyclos.utils.Dimensions;
import nl.strohalm.cyclos.utils.ImageHelper.ImageType;
import nl.strohalm.cyclos.utils.RelationshipHelper;
import nl.strohalm.cyclos.utils.query.PageHelper;
import nl.strohalm.cyclos.webservices.model.ImageVO;
import nl.strohalm.cyclos.webservices.utils.ImageHelper;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.io.IOUtils;
import org.springframework.web.context.ServletContextAware;
/**
* Image service implementation
* @author luis
*/
public class ImageServiceImpl implements ImageServiceLocal, InitializingService, ServletContextAware {
private static final String SYSTEM_IMAGES_PATH = "/WEB-INF/images/system";
private static final String STYLE_IMAGES_PATH = "/WEB-INF/images/style";
private static final int MAX_IMAGE_SIZE_MULTIPLIER = 5;
private FetchServiceLocal fetchService;
private ImageDAO imageDao;
private SettingsServiceLocal settingsService;
private ServletContext servletContext;
private CustomizedFileServiceLocal customizedFileService;
private ImageHelper imageHelper;
private static final Dimensions SYSTEM_IMAGE_DIMENSIONS = new Dimensions(3000, 3000);
@Override
public ImageVO getImageVO(final SystemImage systemImage) {
return imageHelper.toVO(systemImage);
}
@Override
public void initializeService() {
if (servletContext == null) {
return;
}
final File systemImagesDir = new File(servletContext.getRealPath(SYSTEM_IMAGES_PATH));
// Import new system images
importNewImages(systemImagesDir.listFiles(), Nature.SYSTEM);
// Only when there are no customized CSS files, import the new style images
CustomizedFileQuery query = new CustomizedFileQuery();
query.setType(CustomizedFile.Type.STYLE);
query.setPageForCount();
boolean hasCssFiles = PageHelper.hasResults(customizedFileService.search(query));
if (!hasCssFiles) {
final File styleImagesDir = new File(servletContext.getRealPath(STYLE_IMAGES_PATH));
importNewImages(styleImagesDir.listFiles(), Nature.STYLE);
}
}
@Override
public List<? extends Image> listByNature(final Nature nature) {
return imageDao.listByNature(nature);
}
@Override
public List<? extends OwneredImage> listByOwner(final Entity owner) {
return imageDao.listByOwner(owner);
}
@Override
public Image load(final Long id, final Relationship... fetch) {
return imageDao.load(id, fetch);
}
/**
* Returns the new system image files
*/
public File[] newSystemImages() {
final File dir = new File(servletContext.getRealPath(SYSTEM_IMAGES_PATH));
return dir.listFiles();
}
@Override
public Image reload(final Long id, final Relationship... fetch) throws EntityNotFoundException {
return imageDao.reload(id, fetch);
}
@Override
public int remove(final Long... ids) {
return imageDao.delete(ids);
}
@Override
public int removeStyleImage(final String imageName) {
try {
Image img = imageDao.load(Image.Nature.STYLE, imageName);
return imageDao.delete(img.getId());
} catch (final EntityNotFoundException e) {
// returns silently
return 0;
}
}
@SuppressWarnings("unchecked")
@Override
public <T extends OwneredImage> T save(final Entity owner, final String caption, final ImageType type, final String name, final InputStream in) {
if (owner instanceof Ad) {
return (T) doSaveAdImage((Ad) owner, caption, type, name, in);
} else if (owner instanceof Member) {
return (T) doSaveMemberImage((Member) owner, caption, type, name, in);
} else {
throw new IllegalArgumentException("Unsupported owner image: " + owner);
}
}
@Override
public Image save(final Nature nature, final ImageType type, final String name, final InputStream in) {
Image image = null;
switch (nature) {
case SYSTEM:
try {
image = load(Image.Nature.SYSTEM, name);
} catch (final EntityNotFoundException e) {
image = new SystemImage();
}
break;
case STYLE:
try {
image = load(Image.Nature.STYLE, name);
} catch (final EntityNotFoundException e) {
image = new StyleImage();
}
break;
case CUSTOM:
try {
image = load(Image.Nature.CUSTOM, name);
} catch (final EntityNotFoundException e) {
image = new CustomImage();
}
break;
default:
throw new IllegalArgumentException("Invalid nature: " + nature);
}
image = save(image, in, type.getContentType(), name);
// We need this reload in order to be able to immediately read the blobs
fetchService.reload(image);
return image;
}
@Override
public void saveDetails(final ImageDetailsDTO details) {
final Entity owner = details.getImageOwner();
int order = 0;
for (final ImageCaptionDTO dto : details.getDetails()) {
final OwneredImage image = imageDao.load(dto.getId());
if (!image.getOwner().equals(owner)) {
throw new PermissionDeniedException();
}
image.setOrder(order++);
image.setCaption(dto.getCaption());
imageDao.update(image);
}
}
public void setCustomizedFileServiceLocal(final CustomizedFileServiceLocal customizedFileService) {
this.customizedFileService = customizedFileService;
}
public void setFetchServiceLocal(final FetchServiceLocal fetchService) {
this.fetchService = fetchService;
}
public void setImageDao(final ImageDAO imageDao) {
this.imageDao = imageDao;
}
public void setImageHelper(final ImageHelper imageHelper) {
this.imageHelper = imageHelper;
}
@Override
public void setServletContext(final ServletContext servletContext) {
this.servletContext = servletContext;
}
public void setSettingsServiceLocal(final SettingsServiceLocal settingsService) {
this.settingsService = settingsService;
}
private AdImage doSaveAdImage(Ad ad, final String caption, final ImageType type, final String name, final InputStream in) {
ad = fetchService.fetch(ad, RelationshipHelper.nested(Ad.Relationships.OWNER, Element.Relationships.GROUP));
final int maxImages = ad.getOwner().getMemberGroup().getMemberSettings().getMaxAdImagesPerMember();
final int count = imageDao.countAdImages(ad);
if (count >= maxImages) {
throw new PermissionDeniedException();
}
final AdImage image = new AdImage();
image.setAd(ad);
image.setCaption(caption);
image.setOrder(count + 1);
return save(image, in, type.getContentType(), name);
}
private MemberImage doSaveMemberImage(Member member, final String caption, final ImageType type, final String name, final InputStream in) {
member = fetchService.fetch(member, Element.Relationships.GROUP);
final int maxImages = member.getMemberGroup().getMemberSettings().getMaxImagesPerMember();
final int count = imageDao.countMemberImages(member);
if (count >= maxImages) {
throw new PermissionDeniedException();
}
final MemberImage image = new MemberImage();
image.setMember(member);
image.setCaption(caption);
image.setOrder(count + 1);
return save(image, in, type.getContentType(), name);
}
/**
* Generates a thumbnail of the original image
* @return The generated temporary files
*/
private Set<File> generateBlobs(final Image image, final InputStream in) throws IOException {
final LocalSettings localSettings = settingsService.getLocalSettings();
// Generate a temporary file
final File originalFile = File.createTempFile("cyclos", "image");
// Store the stream to the file
IOUtils.copy(in, new FileOutputStream(originalFile));
final ImageType type = ImageType.getByContentType(image.getContentType());
File imageFile = null;
File thumbnailFile = null;
// When the runtime cannot handle the given image type, we'd better not try!!!
if (type.isResizeSupported()) {
// Get the limits
final Dimensions maxImageDimensions = image instanceof SystemImage ? SYSTEM_IMAGE_DIMENSIONS : localSettings.getMaxImageDimensions();
final Dimensions maxThumbnailDimensions = localSettings.getMaxThumbnailDimensions();
// Read the image properties
final BufferedImage bufImage = ImageIO.read(originalFile);
final Dimensions originalDimensions = new Dimensions(bufImage.getWidth(), bufImage.getHeight());
if (originalDimensions.isGreaterThan(new Dimensions(maxImageDimensions.getWidth() * MAX_IMAGE_SIZE_MULTIPLIER, maxImageDimensions.getHeight() * MAX_IMAGE_SIZE_MULTIPLIER))) {
throw new ImageException(ImageException.INVALID_DIMENSION);
}
// Image (except style sheet) needs resizing
if (image.getNature() != Image.Nature.STYLE) {
if (originalDimensions.isGreaterThan(maxImageDimensions)) {
imageFile = nl.strohalm.cyclos.utils.ImageHelper.resizeGivenMaxDimensions(bufImage, type.getContentType(), maxImageDimensions);
}
}
// Thumbnail needs resizing
if (originalDimensions.isGreaterThan(maxThumbnailDimensions)) {
thumbnailFile = nl.strohalm.cyclos.utils.ImageHelper.resizeGivenMaxDimensions(bufImage, type.getContentType(), maxThumbnailDimensions);
}
}
// When no resize was done, use the same original file
if (imageFile == null) {
imageFile = originalFile;
}
if (thumbnailFile == null) {
thumbnailFile = originalFile;
}
// Set the blobs
final int imageSize = (int) imageFile.length();
image.setImage(imageDao.createBlob(new FileInputStream(imageFile), imageSize));
image.setImageSize(imageSize);
final int thumbnailSize = (int) thumbnailFile.length();
image.setThumbnail(imageDao.createBlob(new FileInputStream(thumbnailFile), thumbnailSize));
image.setThumbnailSize(thumbnailSize);
// Return the open files
return new HashSet<File>(Arrays.asList(originalFile, imageFile, thumbnailFile));
}
/**
* Imports new images into the database
*/
private void importNewImages(final File[] newImages, final Nature nature) {
for (final File file : newImages) {
final String fileName = file.getName();
ImageType type;
try {
type = ImageType.getByFileName(fileName);
} catch (final Exception e) {
// Ignore...probably not an image file
continue;
}
try {
// Check if the image is in the database
load(nature, fileName);
} catch (final EntityNotFoundException e) {
// Not on DB - update it
try {
save(nature, type, fileName, new FileInputStream(file));
} catch (final FileNotFoundException e1) {
throw new IllegalStateException("File not found?!? " + e1);
}
}
}
}
private Image load(final Nature nature, final String name) {
// Nonsense for images with owner
if (nature.getOwnerProperty() != null) {
throw new EntityNotFoundException(Image.class);
}
return imageDao.load(nature, name);
}
private <I extends Image> I save(I image, final InputStream in, final String contentType, final String name) {
image.setContentType(contentType);
image.setName(name);
image.setLastModified(Calendar.getInstance());
Set<File> openFiles = null;
try {
try {
openFiles = generateBlobs(image, in);
} catch (final ImageException e) {
throw e;
} catch (final Throwable e) {
throw new ImageException(e);
}
if (image.isTransient()) {
image = imageDao.insert(image, true);
} else {
image = imageDao.update(image, true);
}
return image;
} finally {
if (CollectionUtils.isNotEmpty(openFiles)) {
// Ensure open files will be removed after the transaction commits
for (final File file : openFiles) {
file.delete();
}
}
}
}
}