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}