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.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 = "viewTheme-bean", parent = "Uif-ViewTheme"),
71 @BeanTag(name = "kbootTheme-bean", 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(name = "name")
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(name = "directory")
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(name = "imageDirectory")
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(name = "minVersionSuffix")
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 public boolean isIncludeMinFiles() {
458 return includeMinFiles;
459 }
460
461 /**
462 * Setter for including min files in the CSS and JS lists
463 *
464 * @param includeMinFiles
465 */
466 public void setIncludeMinFiles(boolean includeMinFiles) {
467 this.includeMinFiles = includeMinFiles;
468 }
469
470 /**
471 * File path for the minified CSS file
472 *
473 * <p>
474 * When min file is not set it will be generated by using the theme directory, name, version, and min prefix.
475 * This corresponds to the min file names generated by the theme builder
476 *
477 * For example, with name 'kboot' and version '2.3.0' the min file name will be
478 * '/themes/kboot/stylesheets/kboot.2.3.0.min.css'
479 * </p>
480 *
481 * @return path of min css file
482 */
483 @BeanTagAttribute(name = "minCssFile")
484 public String getMinCssFile() {
485 return minCssFile;
486 }
487
488 /**
489 * Setter for the min CSS file path
490 *
491 * @param minCssFile
492 */
493 public void setMinCssFile(String minCssFile) {
494 this.minCssFile = minCssFile;
495 }
496
497 /**
498 * File path for the minified JS file
499 *
500 * <p>
501 * When min file is not set it will be generated by using the theme directory, name, version, and min prefix.
502 * This corresponds to the min file names generated by the theme builder
503 *
504 * For example, with name 'kboot' and version '2.3.0' the min file name will be
505 * '/themes/kboot/scripts/kboot.2.3.0.min.js'
506 * </p>
507 *
508 * @return path of min css file
509 */
510 @BeanTagAttribute(name = "minScriptFile")
511 public String getMinScriptFile() {
512 return minScriptFile;
513 }
514
515 /**
516 * Setter for the min JS file path
517 *
518 * @param minScriptFile
519 */
520 public void setMinScriptFile(String minScriptFile) {
521 this.minScriptFile = minScriptFile;
522 }
523
524 /**
525 * List of file paths (relative to web root) or URLs that make up the minified CSS file
526 *
527 * <p>
528 * In development mode, instead of sourcing in the min CSS file, the list of files specified here will
529 * be included. This is to facilitate easier debugging. When using the theme builder this list is automatically
530 * retrieved and populated from the theme properties
531 * </p>
532 *
533 * @return list of min CSS file paths or URLs
534 */
535 @BeanTagAttribute(name = "minCssSourceFiles", type = BeanTagAttribute.AttributeType.LISTVALUE)
536 public List<String> getMinCssSourceFiles() {
537 return minCssSourceFiles;
538 }
539
540 /**
541 * Setter for the min file CSS list
542 *
543 * @param minCssSourceFiles
544 */
545 public void setMinCssSourceFiles(List<String> minCssSourceFiles) {
546 this.minCssSourceFiles = minCssSourceFiles;
547 }
548
549 /**
550 * List of file paths (relative to web root) or URLs that make up the minified JS file
551 *
552 * <p>
553 * In development mode, instead of sourcing in the min JS file, the list of files specified here will
554 * be included. This is to facilitate easier debugging. When using the theme builder this list is automatically
555 * retrieved and populated from the theme properties
556 * </p>
557 *
558 * @return list of min JS file paths or URLs
559 */
560 @BeanTagAttribute(name = "minScriptSourceFiles", type = BeanTagAttribute.AttributeType.LISTVALUE)
561 public List<String> getMinScriptSourceFiles() {
562 return minScriptSourceFiles;
563 }
564
565 /**
566 * Setter for the min file JS list
567 *
568 * @param minScriptSourceFiles
569 */
570 public void setMinScriptSourceFiles(List<String> minScriptSourceFiles) {
571 this.minScriptSourceFiles = minScriptSourceFiles;
572 }
573
574 /**
575 * List of file paths (relative to the web root) or URLs that will be sourced into the view
576 * as CSS files
577 *
578 * <p>
579 * Generally this list should be left empty, and the min file lists configured instead (or none with
580 * theme builder). However if there are resources that are not part of the minified CSS file that should
581 * be included with the theme they can be added here
582 *
583 * The minified file path (or list of individual files that make up the minification) will be added
584 * to the beginning of this list. Therefore any entries explicitly added through configuration will be
585 * sourced in last
586 * </p>
587 *
588 * @return list of file paths or URLs for CSS
589 */
590 @BeanTagAttribute(name = "cssFiles", type = BeanTagAttribute.AttributeType.LISTVALUE)
591 public List<String> getCssFiles() {
592 return cssFiles;
593 }
594
595 /**
596 * Setter for the list of CSS files that should be sourced in along with the minified files
597 *
598 * @param cssFiles
599 */
600 public void setCssFiles(List<String> cssFiles) {
601 this.cssFiles = cssFiles;
602 }
603
604 /**
605 * List of file paths (relative to the web root) or URLs that will be sourced into the view
606 * as JS files
607 *
608 * <p>
609 * Generally this list should be left empty, and the min file lists configured instead (or none with
610 * theme builder). However if there are resources that are not part of the minified JS file that should
611 * be included with the theme they can be added here
612 *
613 * The minified file path (or list of individual files that make up the minification) will be added
614 * to the beginning of this list. Therefore any entries explicitly added through configuration will be
615 * sourced in last
616 * </p>
617 *
618 * @return list of file paths or URLs for JS
619 */
620 @BeanTagAttribute(name = "scriptFiles", type = BeanTagAttribute.AttributeType.LISTVALUE)
621 public List<String> getScriptFiles() {
622 return scriptFiles;
623 }
624
625 /**
626 * Setter for the list of JS files that should be sourced in along with the minified files
627 *
628 * @param scriptFiles
629 */
630 public void setScriptFiles(List<String> scriptFiles) {
631 this.scriptFiles = scriptFiles;
632 }
633
634 /**
635 * Indicates whether the theme has been built (or will be built) using the theme builder and therefore
636 * the theme configuration can be defaulted according to the conventions used by the builder
637 *
638 * <p>
639 * When set to true, only the {@link #getName()} property is required to be configured for the theme. All
640 * other configuration will be determined based on convention. When manually configuring the theme, this flag
641 * should be turned off (by default this flag is on)
642 * </p>
643 *
644 * @return true if the theme uses the theme builder, false if not
645 */
646 @BeanTagAttribute(name = "usesThemeBuilder")
647 public boolean isUsesThemeBuilder() {
648 return usesThemeBuilder;
649 }
650
651 /**
652 * Setter the indicates whether the theme uses the theme builder
653 *
654 * @param usesThemeBuilder
655 */
656 public void setUsesThemeBuilder(boolean usesThemeBuilder) {
657 this.usesThemeBuilder = usesThemeBuilder;
658 }
659
660 /**
661 * Helper method to retrieve an instance of {@link org.kuali.rice.core.api.config.property.ConfigurationService}
662 *
663 * @return instance of ConfigurationService
664 */
665 public ConfigurationService getConfigurationService() {
666 return CoreApiServiceLocator.getKualiConfigurationService();
667 }
668
669 }