001/** 002 * Copyright 2005-2016 The Kuali Foundation 003 * 004 * Licensed under the Educational Community License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.opensource.org/licenses/ecl2.php 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016package org.kuali.rice.krad.theme; 017 018import org.apache.commons.lang.StringUtils; 019import org.apache.log4j.Logger; 020import org.codehaus.plexus.util.FileUtils; 021import org.kuali.common.util.Assert; 022import org.kuali.rice.krad.theme.util.ThemeBuilderConstants; 023import org.kuali.rice.krad.theme.util.ThemeBuilderUtils; 024 025import java.io.File; 026import java.io.IOException; 027import java.util.ArrayList; 028import java.util.Arrays; 029import java.util.HashMap; 030import java.util.List; 031import java.util.Map; 032import java.util.Properties; 033 034/** 035 * Helper class for {@link ThemeBuilder} that performs the various overlays during the build process 036 * 037 * <p> 038 * There are three main overlay (copy) processes executed during the theme building: 039 * 040 * <ol> 041 * <li>Copy related assets from web app source to the configured output directory</li> 042 * <li>Overlay assets from a parent theme to a child theme</li> 043 * <li>Perform any additional overlays that are configured in a theme's properties file</li> 044 * </ol> 045 * </p> 046 * 047 * @author Kuali Rice Team (rice.collab@kuali.org) 048 */ 049public class ThemeBuilderOverlays { 050 private static final Logger LOG = Logger.getLogger(ThemeBuilder.class); 051 052 /** 053 * Invoked at the beginning of the build process to move assets from the web source directory to the 054 * output directory, where they can be further processed 055 * 056 * <p> 057 * Note: Not all web resources are copied, just the assets that are needed to build all themes. This includes 058 * all the theme directories, plugin directories, and KRAD scripts directory 059 * </p> 060 * 061 * @param webappSourceDir absolute path to the web source directory 062 * @param themeBuilderOutputDir absolute path to the target directory, where assets will be copied 063 * to and processed. If the directory does not exist it will be created 064 * @param additionalThemeDirectories list of additional theme paths that should be copied to 065 * the output directory 066 * @param additionalPluginDirectories list of additional plugin paths that should be copied to 067 * the output directory 068 * @throws IOException 069 */ 070 protected static void copyAssetsToWorkingDir(String webappSourceDir, String themeBuilderOutputDir, 071 List<String> additionalThemeDirectories, List<String> additionalPluginDirectories) 072 throws IOException { 073 Assert.hasText(themeBuilderOutputDir, "Working directory for theme builder not set"); 074 075 File webappSource = new File(webappSourceDir); 076 if (!webappSource.exists()) { 077 throw new RuntimeException("Webapp source directory does not exist"); 078 } 079 080 File workingDir = new File(themeBuilderOutputDir); 081 if (!workingDir.exists()) { 082 workingDir.mkdir(); 083 } 084 085 workingDir.setWritable(true); 086 087 if (LOG.isDebugEnabled()) { 088 LOG.debug("Copying script, theme, plugin resource to working dir: " + themeBuilderOutputDir); 089 } 090 091 ThemeBuilderUtils.copyDirectory( 092 webappSourceDir + ThemeBuilderConstants.KRAD_SCRIPTS_DIRECTORY, 093 themeBuilderOutputDir + ThemeBuilderConstants.KRAD_SCRIPTS_DIRECTORY); 094 095 ThemeBuilderUtils.copyDirectory( 096 webappSourceDir + ThemeBuilderConstants.DEFAULT_THEMES_DIRECTORY, 097 themeBuilderOutputDir + ThemeBuilderConstants.DEFAULT_THEMES_DIRECTORY); 098 099 if (additionalThemeDirectories != null) { 100 for (String additionalThemeDirectory : additionalThemeDirectories) { 101 ThemeBuilderUtils.copyDirectory(webappSourceDir + additionalThemeDirectory, 102 themeBuilderOutputDir + additionalThemeDirectory); 103 } 104 } 105 106 ThemeBuilderUtils.copyDirectory( 107 webappSourceDir + ThemeBuilderConstants.DEFAULT_PLUGINS_DIRECTORY, 108 themeBuilderOutputDir + ThemeBuilderConstants.DEFAULT_PLUGINS_DIRECTORY); 109 110 if (additionalPluginDirectories != null) { 111 for (String additionalPluginDirectory : additionalPluginDirectories) { 112 ThemeBuilderUtils.copyDirectory(webappSourceDir + additionalPluginDirectory, 113 themeBuilderOutputDir + additionalPluginDirectory); 114 } 115 } 116 } 117 118 /** 119 * Overlays assets from a parent theme (if there is a parent) to a child theme 120 * 121 * <p> 122 * If the given theme has a parent (determined by the parent property in the theme properties), all files 123 * from the parent theme directory are copied to the given theme directory unless: 124 * 125 * <ul> 126 * <li>A file exists in the child theme directory with the same name and relative path</li> 127 * <li>The files has been exluded through the property <code>parentExcludes</code></li> 128 * </ul> 129 * </p> 130 * 131 * @param themeName name of the theme to be processed 132 * @param themeDirectory directory for the theme (parent assets will be copied here) 133 * @param themeProperties properties for the theme, used to retrieve the parent configuration and the 134 * parent excludes 135 * @param themeNamePathMapping mapping of theme names to theme paths, used to find the parent theme 136 * directory path 137 */ 138 protected static void overlayParentAssets(String themeName, File themeDirectory, Properties themeProperties, 139 Map<String, String> themeNamePathMapping) { 140 if (!themeProperties.containsKey(ThemeBuilderConstants.ThemeConfiguration.PARENT)) { 141 return; 142 } 143 144 String parentThemeName = themeProperties.getProperty(ThemeBuilderConstants.ThemeConfiguration.PARENT); 145 if (StringUtils.isBlank(parentThemeName)) { 146 return; 147 } 148 149 LOG.info("Overlaying assets from parent " + parentThemeName + " to child " + themeName); 150 151 String[] parentExcludes = ThemeBuilderUtils.getPropertyValueAsArray( 152 ThemeBuilderConstants.ThemeConfiguration.PARENT_EXCLUDES, themeProperties); 153 154 String parentThemePath = themeNamePathMapping.get(parentThemeName); 155 156 File parentThemeDirectory = new File(parentThemePath); 157 if (!parentThemeDirectory.exists()) { 158 throw new RuntimeException("Parent theme does not exist at path: " + parentThemePath); 159 } 160 161 List<String> copyDirectoryExcludes = new ArrayList<String>(); 162 copyDirectoryExcludes.add(ThemeBuilderConstants.THEME_PROPERTIES_FILE); 163 164 if (parentExcludes != null) { 165 copyDirectoryExcludes.addAll(Arrays.asList(parentExcludes)); 166 } 167 168 try { 169 ThemeBuilderUtils.copyMissingContent(parentThemeDirectory, themeDirectory, copyDirectoryExcludes); 170 } catch (IOException e) { 171 throw new RuntimeException("Unable to copy parent theme directory", e); 172 } 173 } 174 175 /** 176 * Performs any additional overlays that have been configured for the theme (with the 177 * <code>additionalOverlays</code> property) 178 * 179 * <p> 180 * Additional overlays can take any directory or file from the web application, and move into the theme directory 181 * or one of its subdirectores. This is useful if there are dependencies in script that needs to be moved so 182 * they are present for the minified version 183 * </p> 184 * 185 * @param themeDirectory directory for the theme where directories will be copied to 186 * @param themeProperties properties for the theme to process, used to pull the additionalOverlays 187 * configuration 188 * @param webappSourceDir absolute path to the web source directory, if the source overlay directory is not 189 * currently present in the output directory, we need to go back and pull it from source 190 * @param themeBuilderOutputDir absolute path to the output directory, used to pull the source overly directory 191 */ 192 protected static void overlayAdditionalDirs(File themeDirectory, Properties themeProperties, String webappSourceDir, 193 String themeBuilderOutputDir) { 194 if (!themeProperties.containsKey(ThemeBuilderConstants.ThemeConfiguration.ADDITIONAL_OVERLAYS)) { 195 return; 196 } 197 198 String additionalOverlaysStr = themeProperties.getProperty( 199 ThemeBuilderConstants.ThemeConfiguration.ADDITIONAL_OVERLAYS); 200 201 Map<String, String> additionalOverlays = parseAdditionalOverlaysStr(additionalOverlaysStr); 202 203 for (Map.Entry<String, String> overlayMapping : additionalOverlays.entrySet()) { 204 String fromSource = overlayMapping.getKey(); 205 String toThemeDir = overlayMapping.getValue(); 206 207 if (StringUtils.isBlank(fromSource)) { 208 throw new RuntimeException("Invalid additional overlay mapping, from directory is blank"); 209 } 210 211 File sourceFile = null; 212 if (fromSource.startsWith("/")) { 213 // source is relative to web root, first try our working directory and if not there copy 214 // from the original source 215 sourceFile = new File(themeBuilderOutputDir + fromSource); 216 if (!sourceFile.exists()) { 217 sourceFile = new File(webappSourceDir + fromSource); 218 } 219 } else { 220 // source directory is relative to theme directory 221 sourceFile = new File(themeDirectory, fromSource); 222 } 223 224 if (!sourceFile.exists()) { 225 throw new RuntimeException( 226 "Source directory/file for additional overlay does not exist at " + sourceFile.getPath()); 227 } 228 229 File targetDir = null; 230 if (StringUtils.isBlank(toThemeDir)) { 231 targetDir = themeDirectory; 232 } else { 233 if (toThemeDir.startsWith("/")) { 234 toThemeDir = toThemeDir.substring(1); 235 } 236 237 targetDir = new File(themeDirectory, toThemeDir); 238 } 239 240 if (!targetDir.exists()) { 241 targetDir.mkdir(); 242 } 243 244 try { 245 if (sourceFile.isDirectory()) { 246 ThemeBuilderUtils.copyMissingContent(sourceFile, targetDir, null); 247 } else { 248 File targetFile = new File(targetDir, sourceFile.getName()); 249 250 FileUtils.copyFile(sourceFile, targetFile); 251 } 252 } catch (IOException e) { 253 throw new RuntimeException("Unable to perform additional overlay", e); 254 } 255 } 256 } 257 258 /** 259 * Helper method that parses the configuration string for additional overlays into a Map where 260 * the key is the source path and the map value is the target path 261 * 262 * <p> 263 * Each mapping in the string should be separated by a comma. Within the mapping, the source path should 264 * be given, followed by the target path in parenthesis 265 * 266 * ex. sourcePath(targetPath),sourcePath2(targetPath2) 267 * </p> 268 * 269 * @param additionalOverlaysStr string to parse into additional overlay mappings 270 * @return map of additional overlay mappings 271 */ 272 protected static Map<String, String> parseAdditionalOverlaysStr(String additionalOverlaysStr) { 273 Map<String, String> additionalOverlays = new HashMap<String, String>(); 274 275 if (StringUtils.isBlank(additionalOverlaysStr)) { 276 return additionalOverlays; 277 } 278 279 String[] additionalOverlaysArray = additionalOverlaysStr.split(","); 280 for (String additionalOverlay : additionalOverlaysArray) { 281 String fromDir = ""; 282 String toDir = ""; 283 284 if (additionalOverlay.contains("(") && additionalOverlay.contains(")")) { 285 fromDir = StringUtils.substringBefore(additionalOverlay, "("); 286 toDir = StringUtils.substringBetween(additionalOverlay, "(", ")"); 287 } else { 288 fromDir = additionalOverlay; 289 } 290 291 additionalOverlays.put(StringUtils.trimToEmpty(fromDir), StringUtils.trimToEmpty(toDir)); 292 } 293 294 return additionalOverlays; 295 } 296 297}