View Javadoc

1   /**
2    * Copyright 2005-2013 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 <code>krad.dev.mode</code>
46   * configuration variable. In development mode it will source in all the CSS and JS files individually (to
47   * allow for easier debugging). In non-development mode it will source in a minified file. The name for the minified
48   * file can be specified by setting {@link #getMinFileName()}. If not set, it will be formed by using the
49   * {@link #getName()}, {@link #getMinVersionSuffix()}, and min suffix (this is the file name generated by the
50   * theme builder). To indicate the min file should not be sourced in regardless of the environment, set the
51   * property {@link #isIncludeMinFiles()} to false
52   *
53   * The path to the minified file is determined by {@link #getDirectory()}. It this is not set, it is defaulted to
54   * be '/themes' plus the name of the theme (eg '/themes/kboot')
55   * </p>
56   *
57   * <p>
58   * There are two ways the theme can be configured, manual or by convention. If you want to manually configured the
59   * view theme, set {@link #isUsesThemeBuilder()} to false. For dev mode, you must then set the {@link
60   * #getMinCssSourceFiles()} and {@link #getMinScriptSourceFiles()} lists to the theme files. For configuration
61   * by convention, only the theme {@link #getName()} is required. The directory will be assumed to be '/themes/{name}'.
62   * Furthermore the list of min CSS and JS files will be retrieved from the theme.properties file created by the
63   * theme builder
64   * </p>
65   *
66   * @author Kuali Rice Team (rice.collab@kuali.org)
67   */
68  @BeanTags({@BeanTag(name = "viewTheme-bean", parent = "Uif-ViewTheme"),
69          @BeanTag(name = "kbootTheme-bean", parent = "Uif-KbootTheme")})
70  public class ViewTheme extends UifDictionaryBeanBase implements Serializable {
71      private static final long serialVersionUID = 7063256242857896580L;
72      private static final Logger LOG = Logger.getLogger(ViewTheme.class);
73  
74      private String name;
75  
76      private String directory;
77      private String imageDirectory;
78  
79      private String minVersionSuffix;
80      private String minFileName;
81      private boolean includeMinFiles;
82      private List<String> minCssSourceFiles;
83      private List<String> minScriptSourceFiles;
84  
85      private List<String> cssFiles;
86      private List<String> scriptFiles;
87  
88      private boolean usesThemeBuilder;
89  
90      public ViewTheme() {
91          super();
92  
93          this.includeMinFiles = true;
94          this.minCssSourceFiles = new ArrayList<String>();
95          this.minScriptSourceFiles = new ArrayList<String>();
96  
97          this.cssFiles = new ArrayList<String>();
98          this.scriptFiles = new ArrayList<String>();
99  
100         this.usesThemeBuilder = true;
101     }
102 
103     /**
104      * Invoked by View#performApplyModel method to setup defaults for the theme
105      *
106      * <p>
107      * Checks whether we are in dev mode, if so it adds all the CSS and JS files as resources. If
108      * {@link #isUsesThemeBuilder()} is true, retrieve the theme-derived.properties file in the theme
109      * directory to get the listing of CSS and JS files for theme
110      *
111      * When not in dev mode, builds the min file name and path for CSS and JS, which is added to
112      * the list that is sourced in
113      * </p>
114      */
115     public void configureThemeDefaults() {
116         boolean inDevMode = getConfigurationService().getPropertyValueAsBoolean(
117                 KRADConstants.ConfigParameters.KRAD_DEV_MODE);
118 
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.minFileName)) {
132                 if (StringUtils.isBlank(this.minVersionSuffix)) {
133                     this.minVersionSuffix = getConfigurationService().getPropertyValueAsString(
134                             KRADConstants.ConfigParameters.APPLICATION_VERSION);
135                 }
136 
137                 this.minFileName = this.name + "." + this.minVersionSuffix + UifConstants.FileExtensions.MIN;
138             }
139 
140             String themeDirectory = getThemeDirectory();
141 
142             this.cssFiles.add(0,
143                     themeDirectory + UifConstants.DEFAULT_STYLESHEETS_DIRECTORY + "/" + this.minFileName
144                             + UifConstants.FileExtensions.CSS);
145             this.scriptFiles.add(0,
146                     themeDirectory + UifConstants.DEFAULT_SCRIPTS_DIRECTORY + "/" + this.minFileName
147                             + UifConstants.FileExtensions.JS);
148         }
149     }
150 
151     /**
152      * Retrieves the directory associated with the theme
153      *
154      * <p>
155      * If {@link #getDirectory()} is not configured, the theme directory is assumed to be located in the
156      * 'themes' folder of the web root. The directory name is assumed to be the name of the theme
157      * </p>
158      *
159      * @return String path to theme directory relative to the web root
160      */
161     public String getThemeDirectory() {
162         String themeDirectory = "";
163 
164         if (StringUtils.isNotBlank(this.directory)) {
165             if (this.directory.startsWith("/")) {
166                 this.directory = this.directory.substring(1);
167             }
168 
169             themeDirectory = this.directory;
170         } else {
171             themeDirectory = UifConstants.DEFAULT_THEMES_DIRECTORY.substring(1) + "/" + this.name + "/";
172         }
173 
174         return themeDirectory;
175     }
176 
177     /**
178      * Sets the {@link #getMinScriptSourceFiles()} and {@link #getMinCssSourceFiles()} lists from the
179      * corresponding properties in the theme properties file
180      */
181     protected void setMinFileLists() {
182         Properties themeProperties = null;
183         try {
184             themeProperties = getThemeProperties();
185         } catch (IOException e) {
186             throw new RuntimeException("Unable to retrieve theme properties for theme: " + this.name);
187         }
188 
189         if (themeProperties == null) {
190             LOG.warn("No theme properties file found for theme with name: " + this.name);
191 
192             return;
193         }
194 
195         String[] cssFiles = getPropertyValue(themeProperties, UifConstants.THEME_CSS_FILES);
196 
197         if (cssFiles != null) {
198             for (String cssFile : cssFiles) {
199                 this.minCssSourceFiles.add(cssFile);
200             }
201         }
202 
203         String[] jsFiles = getPropertyValue(themeProperties, UifConstants.THEME_JS_FILES);
204 
205         if (jsFiles != null) {
206             for (String jsFile : jsFiles) {
207                 this.minScriptSourceFiles.add(jsFile);
208             }
209         }
210     }
211 
212     /**
213      * Retrieves the theme properties associated with the theme
214      *
215      * <p>
216      * The theme builder creates a file named {@link org.kuali.rice.krad.uif.UifConstants#THEME_DERIVED_PROPERTY_FILE}
217      * located in the theme directory. Here the path is formed and loaded into a properties object
218      * </p>
219      *
220      * @return Properties object containing theme properties, or null if the properties file was not found
221      * @throws IOException
222      */
223     protected Properties getThemeProperties() throws IOException {
224         Properties themeProperties = null;
225 
226         String appUrl = getConfigurationService().getPropertyValueAsString(KRADConstants.ConfigParameters.APPLICATION_URL);
227         String propertiesUrlPath = appUrl + "/" + getThemeDirectory() + "/"
228                 + UifConstants.THEME_DERIVED_PROPERTY_FILE;
229 
230         InputStream inputStream = null;
231         try {
232             URL propertiesUrl = new URL(propertiesUrlPath);
233             inputStream = propertiesUrl.openStream();
234 
235             themeProperties = new Properties();
236             themeProperties.load(inputStream);
237         } finally {
238             if (inputStream != null) {
239                 inputStream.close();
240             }
241         }
242 
243         return themeProperties;
244     }
245 
246     /**
247      * Helper method to retrieve the value of a property from the given Properties object as a
248      * string array (string is parsed using comma delimiter)
249      *
250      * @param properties properties object to pull property value from
251      * @param key key for the property to retrieve
252      * @return string array parsed from the property value, or null if property was not found or empty
253      */
254     protected String[] getPropertyValue(Properties properties, String key) {
255         String[] propertyValueArray = null;
256 
257         if (properties.containsKey(key)) {
258             String propertyValueString = properties.getProperty(key);
259 
260             if (propertyValueString != null) {
261                 propertyValueArray = propertyValueString.split(",");
262             }
263         }
264 
265         return propertyValueArray;
266     }
267 
268     /**
269      * A name that identifies the view theme, when using the theme builder this should be the same as
270      * the directory (for example, if directory is '/themes/kboot', the theme name will be 'kboot')
271      *
272      * <p>
273      * <b>When using the theme builder (config by convention), the name is required configuration</b>
274      * </p>
275      *
276      * @return name for the theme
277      */
278     @BeanTagAttribute(name = "name")
279     public String getName() {
280         return name;
281     }
282 
283     /**
284      * Setter for the theme name
285      *
286      * @param name
287      */
288     public void setName(String name) {
289         this.name = name;
290     }
291 
292     /**
293      * Path to the directory (relative to the web root) that holds the assets for the theme
294      *
295      * <p>
296      * When using the theme builder the directory is not required and will default to '/themes/{name}'
297      * </p>
298      *
299      * @return path to theme directory
300      */
301     @BeanTagAttribute(name = "directory")
302     public String getDirectory() {
303         return directory;
304     }
305 
306     /**
307      * Setter for the theme directory path
308      *
309      * @param directory
310      */
311     public void setDirectory(String directory) {
312         this.directory = directory;
313     }
314 
315     /**
316      * Path to the directory (relative to the web root) that contains images for the theme
317      *
318      * <p>
319      * Configured directory will populate the {@link org.kuali.rice.krad.uif.UifConstants.ContextVariableNames#THEME_IMAGES}
320      * context variable which can be referenced with an expression for an image source
321      * </p>
322      *
323      * <p>
324      * When using the theme builder the image directory is not required and will default to a sub directory of the
325      * theme directory with name 'images'
326      * </p>
327      *
328      * @return theme image directory
329      */
330     @BeanTagAttribute(name = "imageDirectory")
331     public String getImageDirectory() {
332         if (StringUtils.isBlank(this.imageDirectory)) {
333             String appUrl = getConfigurationService().getPropertyValueAsString(KRADConstants.ConfigParameters.APPLICATION_URL);
334 
335             this.imageDirectory = appUrl + "/" + getThemeDirectory() + UifConstants.DEFAULT_IMAGES_DIRECTORY + "/";
336         }
337 
338         return imageDirectory;
339     }
340 
341     /**
342      * Setter for the directory that contains images for the theme
343      *
344      * @param imageDirectory
345      */
346     public void setImageDirectory(String imageDirectory) {
347         this.imageDirectory = imageDirectory;
348     }
349 
350     /**
351      * When {@link #getMinFileName()} is not set, the min file name will be generated using the theme
352      * name, version, and the min suffix. This property is set to indicate the version number to use
353      *
354      * <p>
355      * For application themes this can be set to the config parameter ${app.version}
356      * </p>
357      *
358      * @return version string for the min file name
359      */
360     @BeanTagAttribute(name = "minVersionSuffix")
361     public String getMinVersionSuffix() {
362         return minVersionSuffix;
363     }
364 
365     /**
366      * Setter for the min file version string
367      *
368      * @param minVersionSuffix
369      */
370     public void setMinVersionSuffix(String minVersionSuffix) {
371         this.minVersionSuffix = minVersionSuffix;
372     }
373 
374     /**
375      * File name (not including path and suffix) for the minified file
376      *
377      * <p>
378      * When min file name is not set it will be generated by using the theme name, version, and min prefix.
379      * This corresponds to the min file names generated by the theme builder
380      *
381      * For example, with name 'kboot' and version '2.3.0' the min file name will be 'kboot.2.3.0.min.js'
382      * </p>
383      *
384      * @return name of min file
385      */
386     @BeanTagAttribute(name = "minFileName")
387     public String getMinFileName() {
388         return minFileName;
389     }
390 
391     /**
392      * Setter for the min file name
393      *
394      * @param minFileName
395      */
396     public void setMinFileName(String minFileName) {
397         this.minFileName = minFileName;
398     }
399 
400     /**
401      * Indicates the min files should be sourced into the CSS and JS lists when not in development mode (this
402      * is regardless of whether theme builder is being used or not)
403      *
404      * <p>
405      * Default is true for including min files
406      * </p>
407      *
408      * @return true if min files should be sourced in, false if not
409      */
410     public boolean isIncludeMinFiles() {
411         return includeMinFiles;
412     }
413 
414     /**
415      * Setter for including min files in the CSS and JS lists
416      *
417      * @param includeMinFiles
418      */
419     public void setIncludeMinFiles(boolean includeMinFiles) {
420         this.includeMinFiles = includeMinFiles;
421     }
422 
423     /**
424      * List of file paths (relative to web root) or URLs that make up the minified CSS file
425      *
426      * <p>
427      * In development mode, instead of sourcing in the min CSS file, the list of files specified here will
428      * be included. This is to facilitate easier debugging. When using the theme builder this list is automatically
429      * retrieved and populated from the theme properties
430      * </p>
431      *
432      * @return list of min CSS file paths or URLs
433      */
434     @BeanTagAttribute(name = "minCssSourceFiles", type = BeanTagAttribute.AttributeType.LISTVALUE)
435     public List<String> getMinCssSourceFiles() {
436         return minCssSourceFiles;
437     }
438 
439     /**
440      * Setter for the min file CSS list
441      *
442      * @param minCssSourceFiles
443      */
444     public void setMinCssSourceFiles(List<String> minCssSourceFiles) {
445         this.minCssSourceFiles = minCssSourceFiles;
446     }
447 
448     /**
449      * List of file paths (relative to web root) or URLs that make up the minified JS file
450      *
451      * <p>
452      * In development mode, instead of sourcing in the min JS file, the list of files specified here will
453      * be included. This is to facilitate easier debugging. When using the theme builder this list is automatically
454      * retrieved and populated from the theme properties
455      * </p>
456      *
457      * @return list of min JS file paths or URLs
458      */
459     @BeanTagAttribute(name = "minScriptSourceFiles", type = BeanTagAttribute.AttributeType.LISTVALUE)
460     public List<String> getMinScriptSourceFiles() {
461         return minScriptSourceFiles;
462     }
463 
464     /**
465      * Setter for the min file JS list
466      *
467      * @param minScriptSourceFiles
468      */
469     public void setMinScriptSourceFiles(List<String> minScriptSourceFiles) {
470         this.minScriptSourceFiles = minScriptSourceFiles;
471     }
472 
473     /**
474      * List of file paths (relative to the web root) or URLs that will be sourced into the view
475      * as CSS files
476      *
477      * <p>
478      * Generally this list should be left empty, and the min file lists configured instead (or none with
479      * theme builder). However if there are resources that are not part of the minified CSS file that should
480      * be included with the theme they can be added here
481      *
482      * The minified file path (or list of individual files that make up the minification) will be added
483      * to the beginning of this list. Therefore any entries explicitly added through configuration will be
484      * sourced in last
485      * </p>
486      *
487      * @return list of file paths or URLs for CSS
488      */
489     @BeanTagAttribute(name = "cssFiles", type = BeanTagAttribute.AttributeType.LISTVALUE)
490     public List<String> getCssFiles() {
491         return cssFiles;
492     }
493 
494     /**
495      * Setter for the list of CSS files that should be sourced in along with the minified files
496      *
497      * @param cssFiles
498      */
499     public void setCssFiles(List<String> cssFiles) {
500         this.cssFiles = cssFiles;
501     }
502 
503     /**
504      * List of file paths (relative to the web root) or URLs that will be sourced into the view
505      * as JS files
506      *
507      * <p>
508      * Generally this list should be left empty, and the min file lists configured instead (or none with
509      * theme builder). However if there are resources that are not part of the minified JS file that should
510      * be included with the theme they can be added here
511      *
512      * The minified file path (or list of individual files that make up the minification) will be added
513      * to the beginning of this list. Therefore any entries explicitly added through configuration will be
514      * sourced in last
515      * </p>
516      *
517      * @return list of file paths or URLs for JS
518      */
519     @BeanTagAttribute(name = "scriptFiles", type = BeanTagAttribute.AttributeType.LISTVALUE)
520     public List<String> getScriptFiles() {
521         return scriptFiles;
522     }
523 
524     /**
525      * Setter for the list of JS files that should be sourced in along with the minified files
526      *
527      * @param scriptFiles
528      */
529     public void setScriptFiles(List<String> scriptFiles) {
530         this.scriptFiles = scriptFiles;
531     }
532 
533     /**
534      * Indicates whether the theme has been built (or will be built) using the theme builder and therefore
535      * the theme configuration can be defaulted according to the conventions used by the builder
536      *
537      * <p>
538      * When set to true, only the {@link #getName()} property is required to be configured for the theme. All
539      * other configuration will be determined based on convention. When manually configuring the theme, this flag
540      * should be turned off (by default this flag is on)
541      * </p>
542      *
543      * @return true if the theme uses the theme builder, false if not
544      */
545     @BeanTagAttribute(name = "usesThemeBuilder")
546     public boolean isUsesThemeBuilder() {
547         return usesThemeBuilder;
548     }
549 
550     /**
551      * Setter the indicates whether the theme uses the theme builder
552      *
553      * @param usesThemeBuilder
554      */
555     public void setUsesThemeBuilder(boolean usesThemeBuilder) {
556         this.usesThemeBuilder = usesThemeBuilder;
557     }
558 
559     /**
560      * Helper method to retrieve an instance of {@link org.kuali.rice.core.api.config.property.ConfigurationService}
561      *
562      * @return instance of ConfigurationService
563      */
564     public ConfigurationService getConfigurationService() {
565         return CoreApiServiceLocator.getKualiConfigurationService();
566     }
567 
568     /**
569      * Returns a clone of the View Theme.
570      *
571      * @return ViewTheme instance
572      */
573     public <T> T copy() {
574         T copiedClass = null;
575         try {
576             copiedClass = (T) this.getClass().newInstance();
577         } catch (Exception exception) {
578             throw new RuntimeException();
579         }
580 
581         copyProperties(copiedClass);
582 
583         return copiedClass;
584     }
585 
586     /**
587      * Copies the properties over for the copy method.
588      *
589      * @param viewTheme ViewTheme instance to copy properties to
590      */
591     protected <T> void copyProperties(T viewTheme) {
592         super.copyProperties(viewTheme);
593 
594         ViewTheme viewThemeCopy = (ViewTheme) viewTheme;
595 
596         viewThemeCopy.setName(this.name);
597         viewThemeCopy.setDirectory(this.directory);
598         viewThemeCopy.setImageDirectory(this.imageDirectory);
599         viewThemeCopy.setMinVersionSuffix(this.minVersionSuffix);
600         viewThemeCopy.setMinFileName(this.minFileName);
601         viewThemeCopy.setIncludeMinFiles(this.includeMinFiles);
602 
603         if (this.minCssSourceFiles != null) {
604             viewThemeCopy.setMinCssSourceFiles(new ArrayList<String>(this.minCssSourceFiles));
605         }
606 
607         if (this.minScriptSourceFiles != null) {
608             viewThemeCopy.setMinScriptSourceFiles(new ArrayList<String>(this.minScriptSourceFiles));
609         }
610 
611         if (this.cssFiles != null) {
612             viewThemeCopy.setCssFiles(new ArrayList<String>(this.cssFiles));
613         }
614 
615         if (this.scriptFiles != null) {
616             viewThemeCopy.setScriptFiles(new ArrayList<String>(this.scriptFiles));
617         }
618 
619         viewThemeCopy.setUsesThemeBuilder(this.usesThemeBuilder);
620     }
621 }