/* 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.utils; import java.io.File; import java.io.FileFilter; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.concurrent.Callable; import javax.servlet.ServletContext; import nl.strohalm.cyclos.entities.Relationship; import nl.strohalm.cyclos.entities.customization.binaryfiles.BinaryFile; import nl.strohalm.cyclos.entities.customization.documents.DynamicDocument; import nl.strohalm.cyclos.entities.customization.documents.StaticDocument; import nl.strohalm.cyclos.entities.customization.files.CustomizedFile; import nl.strohalm.cyclos.entities.customization.files.CustomizedFile.Type; import nl.strohalm.cyclos.entities.customization.files.CustomizedFileQuery; import nl.strohalm.cyclos.entities.exceptions.EntityNotFoundException; import nl.strohalm.cyclos.entities.groups.Group; import nl.strohalm.cyclos.entities.groups.GroupFilter; import nl.strohalm.cyclos.entities.groups.OperatorGroup; import nl.strohalm.cyclos.entities.members.Element; import nl.strohalm.cyclos.services.groups.GroupService; import nl.strohalm.cyclos.services.settings.SettingsService; import nl.strohalm.cyclos.utils.access.LoggedUser; import nl.strohalm.cyclos.utils.conversion.LocaleConverter; import nl.strohalm.cyclos.utils.customizedfile.CustomizedFileHandler; import org.apache.commons.io.filefilter.DirectoryFileFilter; import org.apache.commons.lang.StringUtils; import org.springframework.web.context.ServletContextAware; /** * Helper class for customizations * @author luis */ public class CustomizationHelper implements ServletContextAware { public class CustomizationData { private CustomizationLevel level; private Long id; public CustomizationData(final CustomizationLevel level) { this.level = level; } public CustomizationData(final CustomizationLevel level, final Long id) { this(level); this.id = id; } public Long getId() { return id; } public CustomizationLevel getLevel() { return level; } } public static enum CustomizationLevel { GROUP, GROUP_FILTER, GLOBAL, NONE } public static List<String> OPERATOR_SPECIFIC_FILES = Collections.unmodifiableList(Arrays.asList("posweb_header.jsp", "posweb_footer.jsp")); public static final String APPLICATION_PAGES_PATH = "/pages/"; public static final String STATIC_FILES_PATH = "/pages/general/static_files/"; public static final String STYLE_PATH = "/pages/styles/"; public static final String HELP_PATH = "/pages/general/translation_files/helps/"; public static final String DOCUMENT_PATH = "/pages/documents/"; private static final List<String> EXCLUDED_DIRS = Arrays.asList(new String[] { "/general/translation_files/", "/scripts/", "/styles/" }); private static final List<String> LOGIN_CUSTOMIZED_FILES = Arrays.asList(new String[] { "login.css", "login.jsp", "top.jsp" }); private GroupService groupService; private SettingsService settingsService; private CustomizedFileHandler customizedFileHandler; private ServletContext context; /** * Returns the real file for the given customized file */ public File customizedFileOf(final CustomizedFile file) { Group group = file.getGroup(); final GroupFilter groupFilter = file.getGroupFilter(); final Type type = file.getType(); String customizedPath; if (group == null && groupFilter == null) { customizedPath = customizedPathFor(type); } else if (group != null) { group = loadGroup(group.getId()); final boolean forceSameGroup = (group instanceof OperatorGroup && OPERATOR_SPECIFIC_FILES.contains(file.getName())); customizedPath = customizedPathFor(type, group, forceSameGroup); } else { customizedPath = customizedPathFor(type, groupFilter); } final File dir = new File(context.getRealPath(customizedPath)); final String fileName = resolveName(file); return new File(dir, fileName); } /** * Returns the real file for the given customized file */ public File customizedFileOf(final CustomizedFile.Type type, final String name) { final CustomizedFile file = new CustomizedFile(); file.setType(type); file.setName(name); return customizedFileOf(file); } /** * Returns the root path for the customized file type */ public String customizedPathFor(final CustomizedFile.Type type) { String path; switch (type) { case STATIC_FILE: path = STATIC_FILES_PATH + "customized/"; break; case HELP: path = HELP_PATH; break; case STYLE: path = STYLE_PATH; break; case APPLICATION_PAGE: path = APPLICATION_PAGES_PATH; break; default: throw new IllegalArgumentException("Unknown file type: " + type); } return path; } /** * Returns the root path for the customized file type */ public String customizedPathFor(final CustomizedFile.Type type, final Group group, final boolean forceSameGroup) { // Style sheets use the same path as the original file if (group != null && type != CustomizedFile.Type.STYLE) { final String pathPart = pathPart(group, forceSameGroup); return customizedPathFor(type) + pathPart + "/"; } return customizedPathFor(type); } /** * Returns the root path for the customized file type */ public String customizedPathFor(final CustomizedFile.Type type, final GroupFilter groupFilter) { // Style sheets use the same path as the original file if (groupFilter != null && type != CustomizedFile.Type.STYLE) { final String pathPart = pathPart(groupFilter); return customizedPathFor(type) + pathPart + "/"; } return customizedPathFor(type); } /** * Deletes the given file on disk */ public void deleteFile(final java.io.File file) { customizedFileHandler.delete(getRelativePath(file)); } /** * Returns the real directory for documents */ public File documentDir() { return new File(context.getRealPath(DOCUMENT_PATH)); } /** * Returns the real document file for a given document */ public File documentFile(final DynamicDocument document) { return new File(documentDir(), "document_" + document.getId() + ".jsp"); } /** * Finds the customization level and the related id for the given parameters, trying files on the following order: * <ol> * <li>Customized for the given group (if any)</li> * <li>Customized for the group filters of the given group (if the group is the same as the logged member's group)</li> * <li>Customized for the given group filter (if any)</li> * <li>Customized globally</li> * <li>The original file (with no customizations)</li> * </ol> */ public CustomizationData findCustomizationOf(final Type type, Group group, final GroupFilter groupFilter, final String name) { final CustomizedFile file = new CustomizedFile(); file.setType(type); file.setName(name); if (group == null && LoggedUser.hasUser()) { group = LoggedUser.group(); } // Try customized for group if (group != null) { group = loadGroup(group.getId()); file.setGroup(group); // Try directly for group String customizedPath = customizedPathFor(type, group, true); String dir = context.getRealPath(customizedPath); String fileName = resolveName(file); File physicalFile = new File(dir, fileName); if (physicalFile.exists()) { if (type == Type.STYLE && group instanceof OperatorGroup) { // fileName already contains correct memberGroup style sheet, but we need to get // the memberGroup instance now for new CustomizationData() to work correctly group = ((OperatorGroup) group).getMember().getGroup(); } return new CustomizationData(CustomizationLevel.GROUP, group.getId()); } // For operator group, try by member group try { if (group instanceof OperatorGroup) { customizedPath = customizedPathFor(type, group, false); dir = context.getRealPath(customizedPath); fileName = resolveName(file); physicalFile = new File(dir, fileName); if (physicalFile.exists()) { // Check if the file found uses the operator or member group final String groupIdPart = "group_" + group.getId(); if (!fileName.contains(groupIdPart) && !dir.contains(groupIdPart)) { // It is the member group group = ((OperatorGroup) group).getMember().getGroup(); } return new CustomizationData(CustomizationLevel.GROUP, group.getId()); } } } catch (final EntityNotFoundException e) { // Ignore } // Try by group filters of the given group (when is the same group as the logged user or no user logged in) if (!LoggedUser.hasUser() || LoggedUser.group().equals(group)) { file.setGroup(null); try { Group groupForFilters = group; if (group instanceof OperatorGroup) { groupForFilters = ((OperatorGroup) group).getMember().getGroup(); } final Collection<GroupFilter> groupFilters = loadGroup(groupForFilters.getId(), Group.Relationships.GROUP_FILTERS).getGroupFilters(); for (final GroupFilter current : groupFilters) { file.setGroupFilter(current); customizedPath = customizedPathFor(type, current); dir = context.getRealPath(customizedPath); fileName = resolveName(file); physicalFile = new File(dir, fileName); if (physicalFile.exists()) { return new CustomizationData(CustomizationLevel.GROUP_FILTER, current.getId()); } } } catch (final EntityNotFoundException e) { // Ignore } } } // Try directly for group filter if (groupFilter != null) { file.setGroupFilter(groupFilter); final File groupFilterFile = customizedFileOf(file); if (groupFilterFile.exists()) { return new CustomizationData(CustomizationLevel.GROUP_FILTER, groupFilter.getId()); } } // Try customized globally final String globallyCustomizedPath = customizedPathFor(type) + name; final File physicalFile = new File(context.getRealPath(globallyCustomizedPath)); if (physicalFile.exists()) { return new CustomizationData(CustomizationLevel.GLOBAL); } // Return the original return new CustomizationData(CustomizationLevel.NONE); } public File findFileOf(final CustomizedFile.Type type, final Group group, final GroupFilter groupFilter, final String name) { final String path = findPathOf(type, group, groupFilter, name); return new File(context.getRealPath(path)); } public File findFileOf(final CustomizedFile.Type type, final Group group, final String name) { return findFileOf(type, group, null, name); } public String findPathOf(final CustomizedFile.Type type, final Group group, final String name) { return findPathOf(type, group, null, name); } /** * Returns the path of a customized file. */ public String findPathOf(final Type type, final Group group, final GroupFilter groupFilter, final String name) { final CustomizationData customization = findCustomizationOf(type, group, groupFilter, name); return pathOf(type, name, customization); } /** * Returns the real form file for a given document */ public File formFile(final DynamicDocument document) { return new File(documentDir(), "form_" + document.getId() + ".jsp"); } public List<File> getDirectoryContents(final String path) { final String rootPath = APPLICATION_PAGES_PATH + ("/".equals(path) ? "" : path); final File rootDir = new File(context.getRealPath(rootPath)); final List<File> allDirectories = Arrays.asList(rootDir.listFiles((FileFilter) DirectoryFileFilter.DIRECTORY)); final List<File> filteredDirectories = new ArrayList<File>(); for (final File currentFile : allDirectories) { final String fullPath = path + currentFile.getName() + "/"; if (!EXCLUDED_DIRS.contains(fullPath)) { filteredDirectories.add(currentFile); } } final List<File> files = Arrays.asList(rootDir.listFiles(CustomizedFile.Type.APPLICATION_PAGE.getFilter())); Collections.sort(filteredDirectories); Collections.sort(files); final List<File> filesAndDirs = new ArrayList<File>(); filesAndDirs.addAll(filteredDirectories); filesAndDirs.addAll(files); return filesAndDirs; } /** * Returns the relative path of the given file according to the servlet context */ public String getRelativePath(final java.io.File file) { final String absolutePath = file.getAbsolutePath(); final String root = context.getRealPath("/"); if (absolutePath.startsWith(root)) { return absolutePath.substring(root.length()); } return absolutePath; } /** * Checks whether any of the the given files is related to the login page */ public boolean isAnyFileRelatedToLoginPage(final Collection<CustomizedFile> files) { for (final CustomizedFile file : files) { if (isRelatedToLoginPage(file)) { return true; } } return false; } /** * Checks whether the given file is related to the login page */ public boolean isRelatedToLoginPage(final CustomizedFile file) { return LOGIN_CUSTOMIZED_FILES.contains(file.getName()); } /** * Lists files for the customized file type */ public List<File> listByType(final CustomizedFile.Type type) { final File dir = new File(context.getRealPath(originalPathFor(type))); return new ArrayList<File>(Arrays.asList(dir.listFiles(type.getFilter()))); } /** * Returns a collection of file names of files that are not already customized */ public List<String> onlyNotAlreadyCustomized(final CustomizedFile.Type type, final List<CustomizedFile> customizedFiles) { final List<String> notYetCustomized = new ArrayList<String>(); final List<File> files = listByType(type); final CustomizedFileQuery query = new CustomizedFileQuery(); query.setType(type); for (final File file : files) { boolean alreadyCustomized = false; for (final CustomizedFile customizedFile : customizedFiles) { if (customizedFile.getName().equals(file.getName())) { alreadyCustomized = true; break; } } if (!alreadyCustomized) { notYetCustomized.add(file.getName()); } } Collections.sort(notYetCustomized); return notYetCustomized; } /** * Returns the real file for the given original file */ public File originalFileOf(final CustomizedFile.Type type, final String name) { final File dir = new File(context.getRealPath(originalPathFor(type))); return new File(dir, name); } /** * Returns the root path for the customized file type */ public String originalPathFor(final CustomizedFile.Type type) { String path; switch (type) { case STATIC_FILE: path = STATIC_FILES_PATH; break; case HELP: final String locale = LocaleConverter.instance().toString(settingsService.getLocalSettings().getLocale()); path = HELP_PATH + locale + "/"; break; case STYLE: path = STYLE_PATH + "original/"; break; case APPLICATION_PAGE: path = APPLICATION_PAGES_PATH; break; default: throw new IllegalArgumentException("Unknown file type: " + type); } return path; } /** * Returns the path for the given customization */ public String pathOf(final Type type, final String name, final CustomizationData customization) { final CustomizedFile file = new CustomizedFile(); file.setType(type); file.setName(name); String path; switch (customization.getLevel()) { case NONE: path = originalPathFor(type); break; case GLOBAL: path = customizedPathFor(type); break; case GROUP: final Group group = EntityHelper.reference(Group.class, customization.getId()); file.setGroup(group); path = customizedPathFor(type, group, true); break; case GROUP_FILTER: final GroupFilter groupFilter = EntityHelper.reference(GroupFilter.class, customization.getId()); file.setGroupFilter(groupFilter); path = customizedPathFor(type, groupFilter); break; default: return null; } return path + resolveName(file); } public void setCustomizedFileHandler(final CustomizedFileHandler customizedFileHandler) { this.customizedFileHandler = customizedFileHandler; } public void setGroupService(final GroupService groupService) { this.groupService = groupService; } @Override public void setServletContext(final ServletContext servletContext) { context = servletContext; } public void setSettingsService(final SettingsService settingsService) { this.settingsService = settingsService; } /** * Returns the real file for a given static document */ public File staticFile(final StaticDocument document) { return new File(documentDir(), document.getId().toString()); } /** * Update a binary file */ public void updateBinaryFile(final java.io.File file, final BinaryFile binaryFile) { byte[] contents; try { contents = binaryFile.getContents().getBytes(1, binaryFile.getSize()); } catch (final Exception e) { throw new IllegalStateException(e); } final Long lastModified = binaryFile.getLastModified() == null ? System.currentTimeMillis() : binaryFile.getLastModified().getTimeInMillis(); customizedFileHandler.write(getRelativePath(file), lastModified, contents); } /** * Update a customized file with binary contents */ public void updateFile(final File file, final long lastModified, final byte[] contents) { customizedFileHandler.write(getRelativePath(file), lastModified, contents); } /** * Update a customized file with text contents */ public void updateFile(final java.io.File file, final long lastModified, String contentsStr) { byte[] contents; if (contentsStr == null) { contentsStr = ""; } try { contents = contentsStr.getBytes("UTF-8"); } catch (final Exception e) { throw new IllegalStateException(e); } updateFile(file, lastModified, contents); } /** * Update a customized file */ public void updateFile(final java.io.File file, final nl.strohalm.cyclos.entities.customization.files.File customizedFile) { final Long lastModified = customizedFile.getLastModified() == null ? System.currentTimeMillis() : customizedFile.getLastModified().getTimeInMillis(); updateFile(file, lastModified, customizedFile.getContents()); } private Group loadGroup(final Long id, final Relationship... relationships) { return LoggedUser.runAsSystem(new Callable<Group>() { @Override public Group call() throws Exception { return groupService.load(id, relationships); } }); } /** * Returns the additional path part for the given customized group */ private String pathPart(final Group group, final boolean forceSameGroup) { if (group == null) { return null; } Long groupId; if (!forceSameGroup && (group instanceof OperatorGroup)) { final OperatorGroup og = (OperatorGroup) loadGroup(group.getId(), RelationshipHelper.nested(OperatorGroup.Relationships.MEMBER, Element.Relationships.GROUP)); groupId = og.getMember().getGroup().getId(); } else { groupId = group.getId(); } return "group_" + groupId.toString(); } /** * Returns the additional path part for the given group filter */ private String pathPart(final GroupFilter groupFilter) { if (groupFilter == null) { return null; } return "group_filter_" + groupFilter.getId(); } /** * Returns the name of the style sheet for a given group */ private String resolveName(final CustomizedFile file) { final Type type = file.getType(); String name = file.getName(); final Group group = file.getGroup(); final GroupFilter groupFilter = file.getGroupFilter(); // Only style sheets may change the name when customized for groups or group filters. Other types change the path when customized if (type != CustomizedFile.Type.STYLE || (group == null && groupFilter == null)) { return name; } String filename; String extension; final int pos = name.lastIndexOf('.'); if (pos < 0) { filename = name; extension = ""; } else { filename = name.substring(0, pos); extension = name.substring(pos + 1); } // Find the path part to append on the name String pathPart; if (group != null) { pathPart = pathPart(group, false); } else { pathPart = pathPart(groupFilter); } name = filename + "_" + pathPart + (StringUtils.isEmpty(extension) ? "" : "." + extension); return name; } }