001/**
002 * Copyright 2005-2018 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.uif.view;
017
018import org.apache.commons.lang.StringUtils;
019import org.apache.log4j.Logger;
020import org.kuali.rice.core.api.CoreApiServiceLocator;
021import org.kuali.rice.core.api.config.property.ConfigurationService;
022import org.kuali.rice.krad.datadictionary.parse.BeanTag;
023import org.kuali.rice.krad.datadictionary.parse.BeanTagAttribute;
024import org.kuali.rice.krad.datadictionary.parse.BeanTags;
025import org.kuali.rice.krad.datadictionary.uif.UifDictionaryBeanBase;
026import org.kuali.rice.krad.uif.UifConstants;
027import org.kuali.rice.krad.util.KRADConstants;
028
029import java.io.File;
030import java.io.IOException;
031import java.io.InputStream;
032import java.io.Serializable;
033import java.net.URL;
034import java.util.ArrayList;
035import java.util.List;
036import java.util.Properties;
037
038/**
039 * Holds a configuration of CSS and JS assets that provides the base for one or more views.
040 *
041 * <p>
042 * The list of CSS and JS files that are sourced in for a view come from its theme, along with any
043 * additional files configured for the specific view. Generally an application will have one theme for the
044 * entire application.
045 *
046 * The theme has logic for 'dev' mode versus 'test/prod' mode. This is controlled through the
047 * {@code rice.krad.dev.mode} configuration variable. In development mode it will source in all the CSS
048 * and JS files individually (to allow for easier debugging). In non-development mode it will source in a
049 * minified file. The path for the minified files can be specified by setting {@link #getMinCssFile()} and
050 * {@link #getMinScriptFile()}. If not set, it will be formed by using the {@link #getName()},
051 * {@link #getMinVersionSuffix()}, and min suffix (this is the file name generated by the theme builder). To
052 * indicate the min file should not be sourced in regardless of the environment, set the property
053 * {@link #isIncludeMinFiles()} to false
054 *
055 * The path to the minified file is determined by {@link #getDirectory()}. It this is not set, it is defaulted to
056 * be '/themes' plus the name of the theme (eg '/themes/kboot')
057 * </p>
058 *
059 * <p>
060 * There are two ways the theme can be configured, manual or by convention. If you want to manually configured the
061 * view theme, set {@link #isUsesThemeBuilder()} to false. For dev mode, you must then set the {@link
062 * #getMinCssSourceFiles()} and {@link #getMinScriptSourceFiles()} lists to the theme files. For configuration
063 * by convention, only the theme {@link #getName()} is required. The directory will be assumed to be '/themes/{name}'.
064 * Furthermore the list of min CSS and JS files will be retrieved from the theme.properties file created by the
065 * theme builder
066 * </p>
067 *
068 * @author Kuali Rice Team (rice.collab@kuali.org)
069 */
070@BeanTags({@BeanTag(name = "theme", parent = "Uif-ViewTheme"),
071        @BeanTag(name = "kbootTheme", parent = "Uif-KbootTheme")})
072public class ViewTheme extends UifDictionaryBeanBase implements Serializable {
073    private static final long serialVersionUID = 7063256242857896580L;
074    private static final Logger LOG = Logger.getLogger(ViewTheme.class);
075
076    private String name;
077
078    private String directory;
079    private String imageDirectory;
080
081    private String minVersionSuffix;
082    private boolean includeMinFiles;
083    private String minCssFile;
084    private String minScriptFile;
085    private List<String> minCssSourceFiles;
086    private List<String> minScriptSourceFiles;
087
088    private List<String> cssFiles;
089    private List<String> scriptFiles;
090
091    private boolean usesThemeBuilder;
092
093    public ViewTheme() {
094        super();
095
096        this.includeMinFiles = true;
097        this.minCssSourceFiles = new ArrayList<String>();
098        this.minScriptSourceFiles = new ArrayList<String>();
099
100        this.cssFiles = new ArrayList<String>();
101        this.scriptFiles = new ArrayList<String>();
102
103        this.usesThemeBuilder = true;
104    }
105
106    /**
107     * Invoked by View#performApplyModel method to setup defaults for the theme
108     *
109     * <p>
110     * Checks whether we are in dev mode, if so it adds all the CSS and JS files as resources. If
111     * {@link #isUsesThemeBuilder()} is true, retrieve the theme-derived.properties file in the theme
112     * directory to get the listing of CSS and JS files for theme
113     *
114     * When not in dev mode, builds the min file name and path for CSS and JS, which is added to
115     * the list that is sourced in
116     * </p>
117     */
118    public void configureThemeDefaults() {
119        // in development mode, use the min source files directly (for debugging)
120        if (inDevMode()) {
121            if (this.usesThemeBuilder) {
122                setMinFileLists();
123            }
124
125            this.cssFiles.addAll(0, this.minCssSourceFiles);
126            this.scriptFiles.addAll(0, this.minScriptSourceFiles);
127        }
128        // when not in development mode and min files are to be sourced in, build the min file
129        // names and push to top of css and script file lists
130        else if (this.includeMinFiles) {
131            if (StringUtils.isBlank(this.minVersionSuffix)) {
132                this.minVersionSuffix = getConfigurationService().getPropertyValueAsString(
133                        KRADConstants.ConfigParameters.APPLICATION_VERSION);
134            }
135
136            String themeDirectory = getThemeDirectory();
137            if (StringUtils.isBlank(this.minCssFile)) {
138                String minCssFileName = this.name
139                        + "."
140                        + this.minVersionSuffix
141                        + UifConstants.FileExtensions.MIN
142                        + UifConstants.FileExtensions.CSS;
143
144                this.minCssFile =
145                        themeDirectory + "/" + UifConstants.DEFAULT_STYLESHEETS_DIRECTORY + "/" + minCssFileName;
146            }
147
148            if (StringUtils.isBlank(this.minScriptFile)) {
149                String minScriptFileName = this.name
150                        + "."
151                        + this.minVersionSuffix
152                        + UifConstants.FileExtensions.MIN
153                        + UifConstants.FileExtensions.JS;
154
155                this.minScriptFile =
156                        themeDirectory + "/" + UifConstants.DEFAULT_SCRIPTS_DIRECTORY + "/" + minScriptFileName;
157            }
158
159            this.cssFiles.add(0, this.minCssFile);
160            this.scriptFiles.add(0, this.minScriptFile);
161        }
162    }
163
164    /**
165     * Retrieves the directory associated with the theme
166     *
167     * <p>
168     * If {@link #getDirectory()} is not configured, the theme directory is assumed to be located in the
169     * 'themes' folder of the web root. The directory name is assumed to be the name of the theme
170     * </p>
171     *
172     * @return String path to theme directory relative to the web root
173     */
174    public String getThemeDirectory() {
175        String themeDirectory;
176
177        if (StringUtils.isNotBlank(this.directory)) {
178            if (this.directory.startsWith("/")) {
179                this.directory = this.directory.substring(1);
180            }
181
182            themeDirectory = this.directory;
183        } else {
184            themeDirectory = UifConstants.DEFAULT_THEMES_DIRECTORY.substring(1) + "/" + this.name;
185        }
186
187        return themeDirectory;
188    }
189
190    /**
191     * Sets the {@link #getMinScriptSourceFiles()} and {@link #getMinCssSourceFiles()} lists from the
192     * corresponding properties in the theme properties file.
193     *
194     * <p>In dev mode, any css files that were generate from Less files are replaced with an include for
195     * the Less file. This is so the less files can be modified directly (without running the theme builder. For
196     * more information see <a href="http://lesscss.org/#usage">Less Usage</a></p>
197     */
198    protected void setMinFileLists() {
199        Properties themeProperties = null;
200        try {
201            themeProperties = getThemeProperties();
202        } catch (IOException e) {
203            throw new RuntimeException("Unable to retrieve theme properties for theme: " + this.name, e);
204        }
205
206        if (themeProperties == null) {
207            LOG.warn("No theme properties file found for theme with name: " + this.name);
208
209            return;
210        }
211
212        String[] cssFiles = getPropertyValue(themeProperties, UifConstants.THEME_CSS_FILES);
213        String[] lessFiles = getPropertyValue(themeProperties, UifConstants.THEME_LESS_FILES);
214
215        if (cssFiles != null) {
216            for (String cssFile : cssFiles) {
217                String includeFile = replaceIncludeWithLessFile(cssFile, lessFiles);
218                this.minCssSourceFiles.add(includeFile);
219            }
220        }
221
222        String[] jsFiles = getPropertyValue(themeProperties, UifConstants.THEME_JS_FILES);
223
224        if (jsFiles != null) {
225            for (String jsFile : jsFiles) {
226                this.minScriptSourceFiles.add(jsFile);
227            }
228        }
229
230        String[] devJSFiles = getPropertyValue(themeProperties, UifConstants.THEME_DEV_JS_FILES);
231
232        if (devJSFiles != null) {
233            for (String jsFile : devJSFiles) {
234                this.minScriptSourceFiles.add(jsFile);
235            }
236        }
237    }
238
239    /**
240     * Retrieves the theme properties associated with the theme
241     *
242     * <p>
243     * The theme builder creates a file named {@link org.kuali.rice.krad.uif.UifConstants#THEME_DERIVED_PROPERTY_FILE}
244     * located in the theme directory. Here the path is formed and loaded into a properties object
245     * </p>
246     *
247     * @return Properties object containing theme properties, or null if the properties file was not found
248     * @throws IOException
249     */
250    protected Properties getThemeProperties() throws IOException {
251        Properties themeProperties = null;
252
253        String appUrl = getConfigurationService().getPropertyValueAsString(
254                KRADConstants.ConfigParameters.APPLICATION_URL);
255        String propertiesUrlPath = appUrl + "/" + getThemeDirectory() + "/" + UifConstants.THEME_DERIVED_PROPERTY_FILE;
256
257        InputStream inputStream = null;
258        try {
259            URL propertiesUrl = new URL(propertiesUrlPath);
260            inputStream = propertiesUrl.openStream();
261
262            themeProperties = new Properties();
263            themeProperties.load(inputStream);
264        } finally {
265            if (inputStream != null) {
266                inputStream.close();
267            }
268        }
269
270        return themeProperties;
271    }
272
273    /**
274     * Helper method to retrieve the value of a property from the given Properties object as a
275     * string array (string is parsed using comma delimiter)
276     *
277     * @param properties properties object to pull property value from
278     * @param key key for the property to retrieve
279     * @return string array parsed from the property value, or null if property was not found or empty
280     */
281    protected String[] getPropertyValue(Properties properties, String key) {
282        String[] propertyValueArray = null;
283
284        if (properties.containsKey(key)) {
285            String propertyValueString = properties.getProperty(key);
286
287            if (propertyValueString != null) {
288                propertyValueArray = propertyValueString.split(",");
289            }
290        }
291
292        return propertyValueArray;
293    }
294
295    /**
296     * Attempts to find a Less file match for the given css file, and if found returns the corresponding Less
297     * file path, otherwise the css path is returned unmodified.
298     *
299     * @param cssFilePath path to css file to find Less files for
300     * @param lessFileNames array of less files names that are available for the theme
301     * @return String path to less file include, or original css file include
302     */
303    protected String replaceIncludeWithLessFile(String cssFilePath, String[] lessFileNames) {
304        if (lessFileNames == null || !includeLess()) {
305            return cssFilePath;
306        }
307
308        for (String lessFileName : lessFileNames) {
309            String lessFileMatch = StringUtils.replace(lessFileName, UifConstants.FileExtensions.LESS,
310                    UifConstants.FileExtensions.CSS);
311
312            if (StringUtils.substringAfterLast(cssFilePath, "/").equals(lessFileMatch)) {
313                return StringUtils.replace(cssFilePath, UifConstants.FileExtensions.CSS,
314                        UifConstants.FileExtensions.LESS);
315            }
316        }
317
318        return cssFilePath;
319    }
320
321    /**
322     * Indicates whether operation is in development mode by checking the KRAD configuration parameter
323     *
324     * @return true if in dev mode, false if not
325     */
326    protected boolean inDevMode() {
327        return getConfigurationService().getPropertyValueAsBoolean(KRADConstants.ConfigParameters.KRAD_DEV_MODE);
328    }
329
330    /**
331     * Indicates whether Less files should be included instead of Css files when running in dev mode.
332     *
333     * @return true if less files should be subsituted, false if not
334     */
335    protected boolean includeLess() {
336        return getConfigurationService().getPropertyValueAsBoolean(KRADConstants.ConfigParameters.KRAD_INCLUDE_LESS);
337    }
338
339    /**
340     * A name that identifies the view theme, when using the theme builder this should be the same as
341     * the directory (for example, if directory is '/themes/kboot', the theme name will be 'kboot')
342     *
343     * <p>
344     * <b>When using the theme builder (config by convention), the name is required configuration</b>
345     * </p>
346     *
347     * @return name for the theme
348     */
349    @BeanTagAttribute
350    public String getName() {
351        return name;
352    }
353
354    /**
355     * Setter for the theme name
356     *
357     * @param name
358     */
359    public void setName(String name) {
360        this.name = name;
361    }
362
363    /**
364     * Path to the directory (relative to the web root) that holds the assets for the theme
365     *
366     * <p>
367     * When using the theme builder the directory is not required and will default to '/themes/{name}'
368     * </p>
369     *
370     * @return path to theme directory
371     */
372    @BeanTagAttribute
373    public String getDirectory() {
374        return directory;
375    }
376
377    /**
378     * Setter for the theme directory path
379     *
380     * @param directory
381     */
382    public void setDirectory(String directory) {
383        this.directory = directory;
384    }
385
386    /**
387     * Path to the directory (relative to the web root) that contains images for the theme
388     *
389     * <p>
390     * Configured directory will populate the {@link org.kuali.rice.krad.uif.UifConstants.ContextVariableNames#THEME_IMAGES}
391     * context variable which can be referenced with an expression for an image source
392     * </p>
393     *
394     * <p>
395     * When using the theme builder the image directory is not required and will default to a sub directory of the
396     * theme directory with name 'images'
397     * </p>
398     *
399     * @return theme image directory
400     */
401    @BeanTagAttribute
402    public String getImageDirectory() {
403        if (StringUtils.isBlank(this.imageDirectory)) {
404            String appUrl = getConfigurationService().getPropertyValueAsString(
405                    KRADConstants.ConfigParameters.APPLICATION_URL);
406
407            this.imageDirectory =
408                    appUrl + "/" + getThemeDirectory() + "/" + UifConstants.DEFAULT_IMAGES_DIRECTORY + "/";
409        }
410
411        return imageDirectory;
412    }
413
414    /**
415     * Setter for the directory that contains images for the theme
416     *
417     * @param imageDirectory
418     */
419    public void setImageDirectory(String imageDirectory) {
420        this.imageDirectory = imageDirectory;
421    }
422
423    /**
424     * When the min file paths are not set, the min file names will be generated using the theme
425     * name, version, and the min suffix. This property is set to indicate the version number to use
426     *
427     * <p>
428     * For application themes this can be set to the config parameter ${app.version}
429     * </p>
430     *
431     * @return version string for the min file name
432     */
433    @BeanTagAttribute
434    public String getMinVersionSuffix() {
435        return minVersionSuffix;
436    }
437
438    /**
439     * Setter for the min file version string
440     *
441     * @param minVersionSuffix
442     */
443    public void setMinVersionSuffix(String minVersionSuffix) {
444        this.minVersionSuffix = minVersionSuffix;
445    }
446
447    /**
448     * Indicates the min files should be sourced into the CSS and JS lists when not in development mode (this
449     * is regardless of whether theme builder is being used or not)
450     *
451     * <p>
452     * Default is true for including min files
453     * </p>
454     *
455     * @return true if min files should be sourced in, false if not
456     */
457    @BeanTagAttribute
458    public boolean isIncludeMinFiles() {
459        return includeMinFiles;
460    }
461
462    /**
463     * Setter for including min files in the CSS and JS lists
464     *
465     * @param includeMinFiles
466     */
467    public void setIncludeMinFiles(boolean includeMinFiles) {
468        this.includeMinFiles = includeMinFiles;
469    }
470
471    /**
472     * File path for the minified CSS file
473     *
474     * <p>
475     * When min file is not set it will be generated by using the theme directory, name, version, and min prefix.
476     * This corresponds to the min file names generated by the theme builder
477     *
478     * For example, with name 'kboot' and version '2.3.0' the min file name will be
479     * '/themes/kboot/stylesheets/kboot.2.3.0.min.css'
480     * </p>
481     *
482     * @return path of min css file
483     */
484    @BeanTagAttribute
485    public String getMinCssFile() {
486        return minCssFile;
487    }
488
489    /**
490     * Setter for the min CSS file path
491     *
492     * @param minCssFile
493     */
494    public void setMinCssFile(String minCssFile) {
495        this.minCssFile = minCssFile;
496    }
497
498    /**
499     * File path for the minified JS file
500     *
501     * <p>
502     * When min file is not set it will be generated by using the theme directory, name, version, and min prefix.
503     * This corresponds to the min file names generated by the theme builder
504     *
505     * For example, with name 'kboot' and version '2.3.0' the min file name will be
506     * '/themes/kboot/scripts/kboot.2.3.0.min.js'
507     * </p>
508     *
509     * @return path of min css file
510     */
511    @BeanTagAttribute
512    public String getMinScriptFile() {
513        return minScriptFile;
514    }
515
516    /**
517     * Setter for the min JS file path
518     *
519     * @param minScriptFile
520     */
521    public void setMinScriptFile(String minScriptFile) {
522        this.minScriptFile = minScriptFile;
523    }
524
525    /**
526     * List of file paths (relative to web root) or URLs that make up the minified CSS file
527     *
528     * <p>
529     * In development mode, instead of sourcing in the min CSS file, the list of files specified here will
530     * be included. This is to facilitate easier debugging. When using the theme builder this list is automatically
531     * retrieved and populated from the theme properties
532     * </p>
533     *
534     * @return list of min CSS file paths or URLs
535     */
536    @BeanTagAttribute
537    public List<String> getMinCssSourceFiles() {
538        return minCssSourceFiles;
539    }
540
541    /**
542     * Setter for the min file CSS list
543     *
544     * @param minCssSourceFiles
545     */
546    public void setMinCssSourceFiles(List<String> minCssSourceFiles) {
547        this.minCssSourceFiles = minCssSourceFiles;
548    }
549
550    /**
551     * List of file paths (relative to web root) or URLs that make up the minified JS file
552     *
553     * <p>
554     * In development mode, instead of sourcing in the min JS file, the list of files specified here will
555     * be included. This is to facilitate easier debugging. When using the theme builder this list is automatically
556     * retrieved and populated from the theme properties
557     * </p>
558     *
559     * @return list of min JS file paths or URLs
560     */
561    @BeanTagAttribute
562    public List<String> getMinScriptSourceFiles() {
563        return minScriptSourceFiles;
564    }
565
566    /**
567     * Setter for the min file JS list
568     *
569     * @param minScriptSourceFiles
570     */
571    public void setMinScriptSourceFiles(List<String> minScriptSourceFiles) {
572        this.minScriptSourceFiles = minScriptSourceFiles;
573    }
574
575    /**
576     * List of file paths (relative to the web root) or URLs that will be sourced into the view
577     * as CSS files
578     *
579     * <p>
580     * Generally this list should be left empty, and the min file lists configured instead (or none with
581     * theme builder). However if there are resources that are not part of the minified CSS file that should
582     * be included with the theme they can be added here
583     *
584     * The minified file path (or list of individual files that make up the minification) will be added
585     * to the beginning of this list. Therefore any entries explicitly added through configuration will be
586     * sourced in last
587     * </p>
588     *
589     * @return list of file paths or URLs for CSS
590     */
591    @BeanTagAttribute
592    public List<String> getCssFiles() {
593        return cssFiles;
594    }
595
596    /**
597     * Setter for the list of CSS files that should be sourced in along with the minified files
598     *
599     * @param cssFiles
600     */
601    public void setCssFiles(List<String> cssFiles) {
602        this.cssFiles = cssFiles;
603    }
604
605    /**
606     * List of file paths (relative to the web root) or URLs that will be sourced into the view
607     * as JS files
608     *
609     * <p>
610     * Generally this list should be left empty, and the min file lists configured instead (or none with
611     * theme builder). However if there are resources that are not part of the minified JS file that should
612     * be included with the theme they can be added here
613     *
614     * The minified file path (or list of individual files that make up the minification) will be added
615     * to the beginning of this list. Therefore any entries explicitly added through configuration will be
616     * sourced in last
617     * </p>
618     *
619     * @return list of file paths or URLs for JS
620     */
621    @BeanTagAttribute
622    public List<String> getScriptFiles() {
623        return scriptFiles;
624    }
625
626    /**
627     * Setter for the list of JS files that should be sourced in along with the minified files
628     *
629     * @param scriptFiles
630     */
631    public void setScriptFiles(List<String> scriptFiles) {
632        this.scriptFiles = scriptFiles;
633    }
634
635    /**
636     * Indicates whether the theme has been built (or will be built) using the theme builder and therefore
637     * the theme configuration can be defaulted according to the conventions used by the builder
638     *
639     * <p>
640     * When set to true, only the {@link #getName()} property is required to be configured for the theme. All
641     * other configuration will be determined based on convention. When manually configuring the theme, this flag
642     * should be turned off (by default this flag is on)
643     * </p>
644     *
645     * @return true if the theme uses the theme builder, false if not
646     */
647    @BeanTagAttribute
648    public boolean isUsesThemeBuilder() {
649        return usesThemeBuilder;
650    }
651
652    /**
653     * Setter the indicates whether the theme uses the theme builder
654     *
655     * @param usesThemeBuilder
656     */
657    public void setUsesThemeBuilder(boolean usesThemeBuilder) {
658        this.usesThemeBuilder = usesThemeBuilder;
659    }
660
661    /**
662     * Helper method to retrieve an instance of {@link org.kuali.rice.core.api.config.property.ConfigurationService}
663     *
664     * @return instance of ConfigurationService
665     */
666    public ConfigurationService getConfigurationService() {
667        return CoreApiServiceLocator.getKualiConfigurationService();
668    }
669
670}