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