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 }