View Javadoc
1   /**
2    * Copyright 2005-2015 The Kuali Foundation
3    *
4    * Licensed under the Educational Community License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    * http://www.opensource.org/licenses/ecl2.php
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   */
16  package org.kuali.rice.krad.theme;
17  
18  import org.apache.commons.lang.StringUtils;
19  import org.apache.log4j.Logger;
20  import org.codehaus.plexus.util.FileUtils;
21  import org.kuali.common.util.Assert;
22  import org.kuali.rice.krad.theme.util.ThemeBuilderConstants;
23  import org.kuali.rice.krad.theme.util.ThemeBuilderUtils;
24  
25  import java.io.File;
26  import java.io.IOException;
27  import java.util.ArrayList;
28  import java.util.Arrays;
29  import java.util.HashMap;
30  import java.util.List;
31  import java.util.Map;
32  import java.util.Properties;
33  
34  /**
35   * Helper class for {@link ThemeBuilder} that performs the various overlays during the build process
36   *
37   * <p>
38   * There are three main overlay (copy) processes executed during the theme building:
39   *
40   * <ol>
41   * <li>Copy related assets from web app source to the configured output directory</li>
42   * <li>Overlay assets from a parent theme to a child theme</li>
43   * <li>Perform any additional overlays that are configured in a theme's properties file</li>
44   * </ol>
45   * </p>
46   *
47   * @author Kuali Rice Team (rice.collab@kuali.org)
48   */
49  public class ThemeBuilderOverlays {
50      private static final Logger LOG = Logger.getLogger(ThemeBuilder.class);
51  
52      /**
53       * Invoked at the beginning of the build process to move assets from the web source directory to the
54       * output directory, where they can be further processed
55       *
56       * <p>
57       * Note: Not all web resources are copied, just the assets that are needed to build all themes. This includes
58       * all the theme directories, plugin directories, and KRAD scripts directory
59       * </p>
60       *
61       * @param webappSourceDir absolute path to the web source directory
62       * @param themeBuilderOutputDir absolute path to the target directory, where assets will be copied
63       * to and processed. If the directory does not exist it will be created
64       * @param additionalThemeDirectories list of additional theme paths that should be copied to
65       * the output directory
66       * @param additionalPluginDirectories list of additional plugin paths that should be copied to
67       * the output directory
68       * @throws IOException
69       */
70      protected static void copyAssetsToWorkingDir(String webappSourceDir, String themeBuilderOutputDir,
71              List<String> additionalThemeDirectories, List<String> additionalPluginDirectories)
72              throws IOException {
73          Assert.hasText(themeBuilderOutputDir, "Working directory for theme builder not set");
74  
75          File webappSource = new File(webappSourceDir);
76          if (!webappSource.exists()) {
77              throw new RuntimeException("Webapp source directory does not exist");
78          }
79  
80          File workingDir = new File(themeBuilderOutputDir);
81          if (!workingDir.exists()) {
82              workingDir.mkdir();
83          }
84  
85          workingDir.setWritable(true);
86  
87          if (LOG.isDebugEnabled()) {
88              LOG.debug("Copying script, theme, plugin resource to working dir: " + themeBuilderOutputDir);
89          }
90  
91          ThemeBuilderUtils.copyDirectory(
92                  webappSourceDir + ThemeBuilderConstants.KRAD_SCRIPTS_DIRECTORY,
93                  themeBuilderOutputDir + ThemeBuilderConstants.KRAD_SCRIPTS_DIRECTORY);
94  
95          ThemeBuilderUtils.copyDirectory(
96                  webappSourceDir + ThemeBuilderConstants.DEFAULT_THEMES_DIRECTORY,
97                  themeBuilderOutputDir + ThemeBuilderConstants.DEFAULT_THEMES_DIRECTORY);
98  
99          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 }