/*
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.themes;
import java.io.File;
import java.io.FileFilter;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FilenameFilter;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.StringReader;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.EnumSet;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
import java.util.zip.ZipOutputStream;
import javax.servlet.ServletContext;
import nl.strohalm.cyclos.entities.customization.files.CustomizedFile;
import nl.strohalm.cyclos.entities.customization.files.CustomizedFile.Type;
import nl.strohalm.cyclos.entities.customization.images.Image;
import nl.strohalm.cyclos.entities.settings.LocalSettings;
import nl.strohalm.cyclos.services.customization.CustomizedFileService;
import nl.strohalm.cyclos.services.customization.ImageService;
import nl.strohalm.cyclos.services.settings.SettingsService;
import nl.strohalm.cyclos.themes.Theme.Style;
import nl.strohalm.cyclos.themes.exceptions.ThemeException;
import nl.strohalm.cyclos.themes.exceptions.ThemeNotFoundException;
import nl.strohalm.cyclos.utils.CSSHelper;
import nl.strohalm.cyclos.utils.CustomizationHelper;
import nl.strohalm.cyclos.utils.ImageHelper.ImageType;
import nl.strohalm.cyclos.utils.WebImageHelper;
import nl.strohalm.cyclos.utils.conversion.CoercionHelper;
import nl.strohalm.cyclos.utils.validation.ValidationException;
import nl.strohalm.cyclos.utils.validation.Validator;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.IOUtils;
import org.apache.commons.io.filefilter.SuffixFileFilter;
import org.apache.commons.lang.StringUtils;
import org.springframework.web.context.ServletContextAware;
/**
* Theme handler implementation
* @author luis
*/
public class ThemeHandlerImpl extends BaseThemeHandler implements ServletContextAware {
private static final String THEME_PROPERTIES_ENTRY = "theme.properties";
private static final String THEMES_PATH = "/WEB-INF/themes/";
private static final FilenameFilter THEME_FILTER;
private static final FilenameFilter STYLE_FILTER;
private static final FileFilter IMAGE_FILTER;
static {
THEME_FILTER = new SuffixFileFilter(".theme");
STYLE_FILTER = new SuffixFileFilter(".css");
IMAGE_FILTER = new FileFilter() {
@Override
public boolean accept(final File pathname) {
// Check if is a recognized image
try {
ImageType.getByContent(pathname);
return true;
} catch (final Exception e) {
return false;
}
}
};
}
/**
* Return the properties for the given zip file
*/
private static Properties properties(final ZipFile zipFile) throws IOException {
final ZipEntry propertiesEntry = zipFile.getEntry(THEME_PROPERTIES_ENTRY);
if (propertiesEntry == null) {
throw new FileNotFoundException(THEME_PROPERTIES_ENTRY);
}
final Properties properties = new Properties();
properties.load(zipFile.getInputStream(propertiesEntry));
return properties;
}
private ServletContext context;
private ImageService imageService;
private CustomizedFileService customizedFileService;
private CustomizationHelper customizationHelper;
private WebImageHelper webImageHelper;
private SettingsService settingsService;
public void setCustomizationHelper(final CustomizationHelper customizationHelper) {
this.customizationHelper = customizationHelper;
}
public void setCustomizedFileService(final CustomizedFileService customizedFileService) {
this.customizedFileService = customizedFileService;
}
public void setImageService(final ImageService imageService) {
this.imageService = imageService;
}
@Override
public void setServletContext(final ServletContext servletContext) {
context = servletContext;
}
public void setSettingsService(final SettingsService settingsService) {
this.settingsService = settingsService;
}
public void setWebImageHelper(final WebImageHelper webImageHelper) {
this.webImageHelper = webImageHelper;
}
@Override
protected void doExport(final Theme theme, final OutputStream out) {
validateForExport(theme);
final ZipOutputStream zipOut = new ZipOutputStream(out);
final LocalSettings settings = settingsService.getLocalSettings();
final String charset = settings.getCharset();
try {
// Retrieve which files will be exported
final List<String> exportedFiles = new ArrayList<String>();
final List<String> exportedImages = new ArrayList<String>();
final Collection<Style> styles = theme.getStyles();
if (styles != null) {
for (final Style style : styles) {
exportedFiles.addAll(style.getFiles());
}
}
// Store the properties file
final Properties properties = asProperties(theme);
zipOut.putNextEntry(new ZipEntry(THEME_PROPERTIES_ENTRY));
properties.store(zipOut, "");
zipOut.closeEntry();
// Store each css file
final List<File> styleFiles = customizationHelper.listByType(CustomizedFile.Type.STYLE);
for (File file : styleFiles) {
// We must use the customized one, not the original
final String name = file.getName();
if (!exportedFiles.contains(name)) {
// When not exporting the given file, continue
continue;
}
// Read the file contents
file = customizationHelper.findFileOf(CustomizedFile.Type.STYLE, null, name);
final String contents = FileUtils.readFileToString(file, charset);
// Resolve the referenced images by reading all url(name) values from the css
exportedImages.addAll(CSSHelper.resolveURLs(contents));
// Write the contents to the zip file
zipOut.putNextEntry(new ZipEntry("styles/" + name));
IOUtils.copy(new StringReader(contents), zipOut, charset);
zipOut.closeEntry();
}
// Store each image
final File dir = webImageHelper.imagePath(Image.Nature.STYLE);
final File[] imageFiles = dir.listFiles(IMAGE_FILTER);
for (final File file : imageFiles) {
final String name = file.getName();
// Export referenced images only
if (!exportedImages.contains(name)) {
continue;
}
zipOut.putNextEntry(new ZipEntry("images/" + name));
IOUtils.copy(new FileInputStream(file), zipOut);
zipOut.closeEntry();
}
} catch (final Exception e) {
throw new ThemeException(e);
} finally {
// Close the stream
IOUtils.closeQuietly(zipOut);
}
}
@Override
protected void doImportNew(final String fileName, final InputStream in) {
final File file = realFile(fileName);
try {
final byte[] data = IOUtils.toByteArray(in);
customizationHelper.updateFile(file, System.currentTimeMillis(), data);
} catch (final Exception e) {
throw new ThemeException();
}
}
@Override
protected List<Theme> doList() {
final String path = context.getRealPath(THEMES_PATH);
final File dir = new File(path);
if (!dir.exists()) {
dir.mkdirs();
}
final File[] files = dir.listFiles(THEME_FILTER);
final List<Theme> themes = new ArrayList<Theme>(files.length);
for (final File file : files) {
try {
themes.add(read(file));
} catch (final ThemeException e) {
// Skip this theme
}
}
Collections.sort(themes);
return themes;
}
@Override
protected void doRemove(final String fileName) {
final File file = realFile(fileName);
if (!file.exists()) {
throw new ThemeNotFoundException(fileName);
}
customizationHelper.deleteFile(file);
}
@Override
protected void doSelect(final String fileName) {
ZipFile zipFile = null;
final LocalSettings settings = settingsService.getLocalSettings();
final String charset = settings.getCharset();
try {
final File file = realFile(fileName);
if (!file.exists()) {
throw new ThemeNotFoundException(fileName);
}
zipFile = new ZipFile(file);
// Ensure the properties entry exists
properties(zipFile);
// Find all currently used images by style
final Map<String, Collection<String>> imagesByFile = new HashMap<String, Collection<String>>();
final File imageDir = webImageHelper.imagePath(Image.Nature.STYLE);
final File[] cssFiles = imageDir.listFiles(STYLE_FILTER);
for (final File css : cssFiles) {
final String contents = FileUtils.readFileToString(css, charset);
final List<String> urls = CSSHelper.resolveURLs(contents);
imagesByFile.put(css.getName(), urls);
}
// Read the files
final Enumeration<? extends ZipEntry> entries = zipFile.entries();
while (entries.hasMoreElements()) {
final ZipEntry entry = entries.nextElement();
// We will not handle directories
if (entry.isDirectory()) {
continue;
}
final String name = entry.getName();
final String entryFileName = new File(name).getName();
if (name.startsWith("images/")) {
final ImageType type = ImageType.getByFileName(entryFileName);
// Save the image
final Image image = imageService.save(Image.Nature.STYLE, type, entryFileName, zipFile.getInputStream(entry));
// Update the physical image
webImageHelper.update(image);
} else if (name.startsWith("styles/")) {
// Save the style sheet
CustomizedFile customizedFile = new CustomizedFile();
customizedFile.setName(entryFileName);
customizedFile.setType(CustomizedFile.Type.STYLE);
final String contents = IOUtils.toString(zipFile.getInputStream(entry), charset);
customizedFile.setContents(contents);
final File originalFile = customizationHelper.originalFileOf(Type.STYLE, entryFileName);
if (originalFile.exists()) {
customizedFile.setOriginalContents(FileUtils.readFileToString(originalFile, charset));
}
customizedFile = customizedFileService.saveForTheme(customizedFile);
// Update the physical file
final File physicalFile = customizationHelper.customizedFileOf(CustomizedFile.Type.STYLE, entryFileName);
customizationHelper.updateFile(physicalFile, customizedFile);
// Remove images that are no longer used
final List<String> newImages = CSSHelper.resolveURLs(contents);
final Collection<String> oldImages = imagesByFile.get(entryFileName);
if (CollectionUtils.isNotEmpty(oldImages)) {
for (final String imageName : oldImages) {
if (!newImages.contains(imageName)) {
// No longer used. Remove it
imageService.removeStyleImage(imageName);
// Remove the physical file
final File imageFile = new File(imageDir, imageName);
customizationHelper.deleteFile(imageFile);
}
}
}
}
}
} catch (final ThemeException e) {
throw e;
} catch (final Exception e) {
throw new ThemeException(e);
} finally {
try {
zipFile.close();
} catch (final Exception e) {
// Ignore
}
}
}
@Override
protected void doValidateForExport(final Theme theme) throws ValidationException {
getExportValidator().validate(theme);
}
/**
* Returns the theme as a properties object
*/
private Properties asProperties(final Theme theme) {
final Properties properties = new Properties();
properties.setProperty("title", StringUtils.trimToEmpty(theme.getTitle()));
properties.setProperty("author", StringUtils.trimToEmpty(theme.getAuthor()));
properties.setProperty("version", StringUtils.trimToEmpty(theme.getVersion()));
properties.setProperty("description", StringUtils.trimToEmpty(theme.getDescription()));
final Collection<String> strings = CoercionHelper.coerceCollection(String.class, theme.getStyles());
properties.setProperty("styles", StringUtils.join(strings.iterator(), ','));
return properties;
}
/**
* Reads properties as a Theme object
*/
private Theme fromProperties(final Properties properties) {
final Theme theme = new Theme();
theme.setTitle(StringUtils.trimToNull(properties.getProperty("title")));
theme.setAuthor(StringUtils.trimToNull(properties.getProperty("author")));
theme.setVersion(StringUtils.trimToNull(properties.getProperty("version")));
theme.setDescription(StringUtils.trimToNull(properties.getProperty("description")));
final String styles = StringUtils.trimToNull(properties.getProperty("styles"));
if (styles == null) {
// None found - Assume all styles
theme.setStyles(EnumSet.allOf(Style.class));
} else {
final String[] array = StringUtils.split(styles, ',');
theme.setStyles(CoercionHelper.coerceCollection(Theme.Style.class, array));
}
return theme;
}
private Validator getExportValidator() {
final Validator exportValidator = new Validator("theme");
exportValidator.property("title").required();
exportValidator.property("filename").required();
exportValidator.property("styles").key("theme.stylesToExport").required();
return exportValidator;
}
/**
* Reads a theme from file
*/
private Theme read(final File file) throws ThemeException {
ZipFile zipFile = null;
try {
if (!file.exists()) {
throw new ThemeNotFoundException(file.getName());
}
zipFile = new ZipFile(file);
final Properties properties = properties(zipFile);
final Theme theme = fromProperties(properties);
theme.setFilename(file.getName());
return theme;
} catch (final Exception e) {
throw new ThemeException(e);
} finally {
try {
zipFile.close();
} catch (final Exception e) {
// Ignore
}
}
}
/**
* Returns the real file for the theme
*/
private File realFile(final String fileName) {
final String path = context.getRealPath(THEMES_PATH);
return new File(path, fileName);
}
}