001/** 002 * Copyright 2005-2016 The Kuali Foundation 003 * 004 * Licensed under the Educational Community License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.opensource.org/licenses/ecl2.php 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016package org.kuali.rice.krad.uif.view; 017 018import org.apache.commons.lang.StringUtils; 019import org.apache.log4j.Logger; 020import org.kuali.rice.core.api.CoreApiServiceLocator; 021import org.kuali.rice.core.api.config.property.ConfigurationService; 022import org.kuali.rice.krad.datadictionary.parse.BeanTag; 023import org.kuali.rice.krad.datadictionary.parse.BeanTagAttribute; 024import org.kuali.rice.krad.datadictionary.parse.BeanTags; 025import org.kuali.rice.krad.datadictionary.uif.UifDictionaryBeanBase; 026import org.kuali.rice.krad.uif.UifConstants; 027import org.kuali.rice.krad.util.KRADConstants; 028 029import java.io.IOException; 030import java.io.InputStream; 031import java.io.Serializable; 032import java.net.URL; 033import java.util.ArrayList; 034import java.util.List; 035import java.util.Properties; 036 037/** 038 * Holds a configuration of CSS and JS assets that provides the base for one or more views 039 * 040 * <p> 041 * The list of CSS and JS files that are sourced in for a view come from its theme, along with any 042 * additional files configured for the specific view. Generally an application will have one theme for the 043 * entire application. 044 * 045 * The theme has logic for 'dev' mode versus 'test/prod' mode. This is controlled through the 046 * {@code rice.krad.dev.mode} configuration variable. In development mode it will source in all the CSS 047 * and JS files individually (to allow for easier debugging). In non-development mode it will source in a 048 * minified file. The path for the minified files can be specified by setting {@link #getMinCssFile()} and 049 * {@link #getMinScriptFile()}. If not set, it will be formed by using the {@link #getName()}, 050 * {@link #getMinVersionSuffix()}, and min suffix (this is the file name generated by the theme builder). To 051 * indicate the min file should not be sourced in regardless of the environment, set the property 052 * {@link #isIncludeMinFiles()} to false 053 * 054 * The path to the minified file is determined by {@link #getDirectory()}. It this is not set, it is defaulted to 055 * be '/themes' plus the name of the theme (eg '/themes/kboot') 056 * </p> 057 * 058 * <p> 059 * There are two ways the theme can be configured, manual or by convention. If you want to manually configured the 060 * view theme, set {@link #isUsesThemeBuilder()} to false. For dev mode, you must then set the {@link 061 * #getMinCssSourceFiles()} and {@link #getMinScriptSourceFiles()} lists to the theme files. For configuration 062 * by convention, only the theme {@link #getName()} is required. The directory will be assumed to be '/themes/{name}'. 063 * Furthermore the list of min CSS and JS files will be retrieved from the theme.properties file created by the 064 * theme builder 065 * </p> 066 * 067 * @author Kuali Rice Team (rice.collab@kuali.org) 068 */ 069@BeanTags({@BeanTag(name = "viewTheme-bean", parent = "Uif-ViewTheme"), 070 @BeanTag(name = "kbootTheme-bean", parent = "Uif-KbootTheme")}) 071public class ViewTheme extends UifDictionaryBeanBase implements Serializable { 072 private static final long serialVersionUID = 7063256242857896580L; 073 private static final Logger LOG = Logger.getLogger(ViewTheme.class); 074 075 private String name; 076 077 private String directory; 078 private String imageDirectory; 079 080 private String minVersionSuffix; 081 private boolean includeMinFiles; 082 private String minCssFile; 083 private String minScriptFile; 084 private List<String> minCssSourceFiles; 085 private List<String> minScriptSourceFiles; 086 087 private List<String> cssFiles; 088 private List<String> scriptFiles; 089 090 private boolean usesThemeBuilder; 091 092 public ViewTheme() { 093 super(); 094 095 this.includeMinFiles = true; 096 this.minCssSourceFiles = new ArrayList<String>(); 097 this.minScriptSourceFiles = new ArrayList<String>(); 098 099 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}