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