View Javadoc

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