View Javadoc
1   /**
2    * Copyright 2005-2016 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.uif.view;
17  
18  import org.apache.commons.lang.StringUtils;
19  import org.apache.log4j.Logger;
20  import org.kuali.rice.core.api.CoreApiServiceLocator;
21  import org.kuali.rice.core.api.config.property.ConfigurationService;
22  import org.kuali.rice.krad.datadictionary.parse.BeanTag;
23  import org.kuali.rice.krad.datadictionary.parse.BeanTagAttribute;
24  import org.kuali.rice.krad.datadictionary.parse.BeanTags;
25  import org.kuali.rice.krad.datadictionary.uif.UifDictionaryBeanBase;
26  import org.kuali.rice.krad.uif.UifConstants;
27  import org.kuali.rice.krad.util.KRADConstants;
28  
29  import java.io.File;
30  import java.io.IOException;
31  import java.io.InputStream;
32  import java.io.Serializable;
33  import java.net.URL;
34  import java.util.ArrayList;
35  import java.util.List;
36  import java.util.Properties;
37  
38  /**
39   * Holds a configuration of CSS and JS assets that provides the base for one or more views.
40   *
41   * <p>
42   * The list of CSS and JS files that are sourced in for a view come from its theme, along with any
43   * additional files configured for the specific view. Generally an application will have one theme for the
44   * entire application.
45   *
46   * The theme has logic for 'dev' mode versus 'test/prod' mode. This is controlled through the
47   * {@code rice.krad.dev.mode} configuration variable. In development mode it will source in all the CSS
48   * and JS files individually (to allow for easier debugging). In non-development mode it will source in a
49   * minified file. The path for the minified files can be specified by setting {@link #getMinCssFile()} and
50   * {@link #getMinScriptFile()}. If not set, it will be formed by using the {@link #getName()},
51   * {@link #getMinVersionSuffix()}, and min suffix (this is the file name generated by the theme builder). To
52   * indicate the min file should not be sourced in regardless of the environment, set the property
53   * {@link #isIncludeMinFiles()} to false
54   *
55   * The path to the minified file is determined by {@link #getDirectory()}. It this is not set, it is defaulted to
56   * be '/themes' plus the name of the theme (eg '/themes/kboot')
57   * </p>
58   *
59   * <p>
60   * There are two ways the theme can be configured, manual or by convention. If you want to manually configured the
61   * view theme, set {@link #isUsesThemeBuilder()} to false. For dev mode, you must then set the {@link
62   * #getMinCssSourceFiles()} and {@link #getMinScriptSourceFiles()} lists to the theme files. For configuration
63   * by convention, only the theme {@link #getName()} is required. The directory will be assumed to be '/themes/{name}'.
64   * Furthermore the list of min CSS and JS files will be retrieved from the theme.properties file created by the
65   * theme builder
66   * </p>
67   *
68   * @author Kuali Rice Team (rice.collab@kuali.org)
69   */
70  @BeanTags({@BeanTag(name = "theme", parent = "Uif-ViewTheme"),
71          @BeanTag(name = "kbootTheme", parent = "Uif-KbootTheme")})
72  public class ViewTheme extends UifDictionaryBeanBase implements Serializable {
73      private static final long serialVersionUID = 7063256242857896580L;
74      private static final Logger LOG = Logger.getLogger(ViewTheme.class);
75  
76      private String name;
77  
78      private String directory;
79      private String imageDirectory;
80  
81      private String minVersionSuffix;
82      private boolean includeMinFiles;
83      private String minCssFile;
84      private String minScriptFile;
85      private List<String> minCssSourceFiles;
86      private List<String> minScriptSourceFiles;
87  
88      private List<String> cssFiles;
89      private List<String> scriptFiles;
90  
91      private boolean usesThemeBuilder;
92  
93      public ViewTheme() {
94          super();
95  
96          this.includeMinFiles = true;
97          this.minCssSourceFiles = new ArrayList<String>();
98          this.minScriptSourceFiles = new ArrayList<String>();
99  
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 }