/** * Copyright © 2014 Instituto Superior Técnico * * This file is part of FenixEdu CMS. * * FenixEdu CMS is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * FenixEdu CMS 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 Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with FenixEdu CMS. If not, see <http://www.gnu.org/licenses/>. */ package org.fenixedu.cms.domain; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.nio.file.Paths; import java.util.Enumeration; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Optional; import java.util.regex.Pattern; import java.util.stream.Stream; import java.util.zip.ZipEntry; import java.util.zip.ZipFile; import java.util.zip.ZipInputStream; import org.fenixedu.bennu.core.domain.Bennu; import org.fenixedu.bennu.core.groups.AnyoneGroup; import org.fenixedu.bennu.io.domain.GroupBasedFile; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.google.common.collect.Lists; import com.google.common.collect.Sets; import com.google.common.io.ByteStreams; import com.google.gson.JsonElement; import com.google.gson.JsonObject; import com.google.gson.JsonParser; import pt.ist.fenixframework.Atomic; import pt.ist.fenixframework.Atomic.TxMode; public class CMSThemeLoader { final static Pattern RELATIVE_PARENT = Pattern.compile("^../|/../|/..$"); final static Pattern RELATIVE_CURRENT = Pattern.compile("^./|/./|/.$"); final static Pattern FULL_PATH = Pattern.compile("^/.*"); public static Logger LOGGER = LoggerFactory.getLogger(CMSThemeLoader.class); private static List<EntryBean> getFolderEntries(ZipInputStream zin) { List<EntryBean> zipEntryBeans = Lists.newArrayList(); ZipEntry zipEntry; try { while ((zipEntry = zin.getNextEntry()) != null) { zipEntryBeans.add(new ZipEntryBean(zin, zipEntry)); } } catch (IOException e) { e.printStackTrace(); } return zipEntryBeans; } public static CMSTheme createFromZipStream(ZipInputStream zin) { return create(getFolderEntries(zin)); } public static CMSTheme createFromZip(ZipFile zipFile) { return create(getZipEntries(zipFile)); } public static CMSTheme createFromFolder(File folder) { return create(getFolderEntries(folder, folder)); } private static List<EntryBean> getFolderEntries(File folder, File root) { List<EntryBean> folderChildren = Lists.newArrayList(); for (File child : folder.listFiles()) { folderChildren.add(new FileEntryBean(child, root)); if (child.isDirectory()) { folderChildren.addAll(getFolderEntries(child, root)); } } return folderChildren; } private static List<EntryBean> getZipEntries(ZipFile zipFile) { List<EntryBean> zipEntryBeans = Lists.newArrayList(); Enumeration<? extends ZipEntry> entries = zipFile.entries(); while (entries.hasMoreElements()) { ZipEntry zipEntry = entries.nextElement(); zipEntryBeans.add(new ZipEntryBean(zipFile, zipEntry)); } return zipEntryBeans; } private static CMSTheme create(List<EntryBean> entries) { JsonObject themeDescription = entries.stream().filter(entry -> entry.getName().endsWith("theme.json")).map(entry -> { return new JsonParser().parse(new String(entry.getContent())).getAsJsonObject(); }).findAny().orElseThrow(() -> new IllegalArgumentException("Theme does not contain a theme.json file!")); CMSThemeFiles themeFiles = new CMSThemeFiles(loadFiles(entries.stream() .filter(entry -> !entry.getName().equals("theme.json") && validName(entry.getName()) && !entry.isDirectory()))); return getOrCreateTheme(themeFiles, themeDescription); } private static boolean validName(String name) { return !(name.contains("__MACOSX") || name.contains("DS_Store") || RELATIVE_PARENT.matcher(name).matches() || RELATIVE_CURRENT.matcher(name).matches() || FULL_PATH.matcher(name).matches()); } @Atomic(mode = TxMode.WRITE) private static CMSTheme getOrCreateTheme(CMSThemeFiles files, JsonObject themeDef) { String themeType = themeDef.get("type").getAsString(); CMSTheme theme = CMSTheme.forType(themeType); if (theme != null) { return theme; } theme = new CMSTheme(); theme.setDescription(themeDef.get("description").getAsString()); theme.setName(themeDef.get("name").getAsString()); theme.setBennu(Bennu.getInstance()); theme.setType(themeType); HashSet<CMSTemplate> refused = Sets.newHashSet(theme.getTemplatesSet()); loadExtends(themeDef, theme); loadPageTemplates(themeDef, theme, refused, files); for (CMSTemplate t : refused) { if (t.getPagesSet().size() != 0) { throw new RuntimeException("Cannot replace theme, '" + t.getType() + "' is being used in some pages but is not included in new theme."); } else { t.delete(); } } theme.setFiles(files); if (themeDef.has("thumbnail")) { CMSThemeFile thumbnail = theme.fileForPath(themeDef.get("thumbnail").getAsString()); theme.setPreviewImage(new GroupBasedFile(thumbnail.getFileName(), thumbnail.getFullPath(), thumbnail.getContent(), AnyoneGroup.get())); theme.setPreviewImagePath(themeDef.get("thumbnail").getAsString()); } if (Bennu.getInstance().getCMSThemesSet().size() == 1) { Bennu.getInstance().setDefaultCMSTheme(theme); } return theme; } private static void loadExtends(JsonObject themeDef, CMSTheme theme) { if (themeDef.has("extends")) { String type = themeDef.get("extends").getAsString(); if (type != null) { CMSTheme parent = CMSTheme.forType(type); if (parent == null) { throw new RuntimeException("Extended theme does not exist"); } else { theme.setExtended(parent); } } } } private static void loadPageTemplates(JsonObject themeDef, CMSTheme theme, HashSet<CMSTemplate> refused, CMSThemeFiles files) { for (Entry<String, JsonElement> entry : themeDef.get("templates").getAsJsonObject().entrySet()) { String type = entry.getKey(); JsonObject obj = entry.getValue().getAsJsonObject(); CMSTemplate tp = Optional.ofNullable(theme.templateForType(type)).orElseGet(() -> new CMSTemplate()); tp.setName(obj.get("name").getAsString()); tp.setDescription(obj.get("description").getAsString()); tp.setType(type); tp.setTheme(theme); if (refused.contains(tp)) { refused.remove(tp); } String path = obj.get("file").getAsString(); CMSThemeFile file = files.getFileForPath(path); tp.setFilePath(path); if (file == null) { throw new RuntimeException("File in template '" + type + "' isn't in the Zip."); } } } private static Map<String, CMSThemeFile> loadFiles(Stream<EntryBean> files) { Map<String, CMSThemeFile> fileMap = new HashMap<>(); files.forEach(bean -> { String filename = bean.getName(); fileMap.put(bean.getName(), new CMSThemeFile(Paths.get(filename).getFileName().toString(), filename, bean.getContent())); }); return fileMap; } private static abstract class EntryBean { private final String name; private final boolean isDirectory; public EntryBean(String name, boolean isDirectory) { this.name = name; this.isDirectory = isDirectory; } public String getName() { return name; } public boolean isDirectory() { return isDirectory; } public abstract byte[] getContent(); } private static class ZipEntryBean extends EntryBean { private final byte[] bytes; public ZipEntryBean(ZipFile zipFile, ZipEntry zipEntry) { super(zipEntry.getName(), zipEntry.isDirectory()); try { this.bytes = ByteStreams.toByteArray(zipFile.getInputStream(zipEntry)); } catch (IOException e) { throw new RuntimeException("Error reading the content of the zip file entry", e); } } public ZipEntryBean(ZipInputStream zin, ZipEntry zipEntry) { super(zipEntry.getName(), zipEntry.isDirectory()); try { this.bytes = ByteStreams.toByteArray(zin); } catch (IOException e) { throw new RuntimeException("Error reading the content of the zip file entry", e); } } @Override public byte[] getContent() { return bytes; } } private static class FileEntryBean extends EntryBean { private final File file; public FileEntryBean(File file, File root) { super(root.toURI().relativize(file.toURI()).getPath(), file.isDirectory()); this.file = file; } @Override public byte[] getContent() { try { return ByteStreams.toByteArray(new FileInputStream(file)); } catch (IOException e) { throw new RuntimeException("Error reading the content of the zip file entry", e); } } } }