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.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 }