001 /** 002 * Copyright 2005-2014 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 */ 016 package org.kuali.rice.krad.uif.util; 017 018 import org.apache.commons.lang.ArrayUtils; 019 import org.apache.commons.lang.StringUtils; 020 import org.kuali.rice.krad.uif.component.Component; 021 import org.kuali.rice.krad.uif.element.Message; 022 import org.kuali.rice.krad.uif.view.View; 023 import org.kuali.rice.krad.util.KRADConstants; 024 025 import java.util.ArrayList; 026 import java.util.Arrays; 027 import java.util.List; 028 029 /** 030 * Rich message structure utilities for parsing message content and converting it to components/content 031 * 032 * @author Kuali Rice Team (rice.collab@kuali.org) 033 */ 034 public class MessageStructureUtils { 035 036 /** 037 * Translate a message with special hooks described in MessageStructureUtils.parseMessage. However, tags which 038 * reference components will not be allowed/translated - only tags which can translate to string content will 039 * be included for this translation. 040 * 041 * @param messageText messageText with only String translateable tags included (no id or component index tags) 042 * @return html translation of rich messageText passed in 043 * @see MessageStructureUtils#parseMessage 044 */ 045 public static String translateStringMessage(String messageText) { 046 if (!StringUtils.isEmpty(messageText)) { 047 List<Component> components = MessageStructureUtils.parseMessage(null, messageText, null, null, false); 048 049 if (!components.isEmpty()) { 050 Component message = components.get(0); 051 052 if (message instanceof Message) { 053 messageText = ((Message) message).getMessageText(); 054 } 055 } 056 } 057 058 return messageText; 059 } 060 061 /** 062 * Parses the message text passed in and returns the resulting rich message component structure. 063 * 064 * <p>If special characters [] are detected the message is split at that location. The types of features supported 065 * by the parse are (note that <> are not part of the content, they specify placeholders): 066 * <ul> 067 * <li>[id=<component id>] - insert component with id specified at that location in the message</li> 068 * <li>[n] - insert component at index n from the inlineComponent list</li> 069 * <li>[<html tag>][/<html tag>] - insert html content directly into the message content at that 070 * location, 071 * without the need to escape the <> characters in xml</li> 072 * <li>[color=<html color code/name>][/color] - wrap content in color tags to make text that color 073 * in the message</li> 074 * <li>[css=<css classes>][/css] - apply css classes specified to the wrapped content - same as wrapping 075 * the content in span with class property set</li> 076 * <li>[link=<href src>][/link] - an easier way to create an anchor that will open in a new page to the 077 * href specified after =</li> 078 * <li>[action=<href src>][/action] - create an action link inline without having to specify a component by 079 * id or index. The options for this are as follows and MUST be in a comma seperated list in the order specified 080 * (specify 1-4 always in this order): 081 * <ul> 082 * <li>methodToCall(String)</li> 083 * <li>validateClientSide(boolean) - true if not set</li> 084 * <li>ajaxSubmit(boolean) - true if not set</li> 085 * <li>successCallback(js function or function declaration) - this only works when ajaxSubmit is true</li> 086 * </ul> 087 * The tag would look something like this [action=methodToCall]Action[/action] in most common cases. And in more 088 * complex cases [action=methodToCall,true,true,functionName]Action[/action]. <p>In addition to these settings, 089 * you can also specify data to send to the server in this fashion (space is required between settings and data): 090 * </p> 091 * [action=<action settings> data={key1: 'value 1', key2: value2}] 092 * </li> 093 * </ul> 094 * If the [] characters are needed in message text, they need to be declared with an escape character: \\[ \\] 095 * </p> 096 * 097 * @param messageId id of the message 098 * @param messageText message text to be parsed 099 * @param componentList the inlineComponent list 100 * @param view the current view 101 * @return list of components representing the parsed message structure 102 */ 103 public static List<Component> parseMessage(String messageId, String messageText, List<Component> componentList, 104 View view, boolean parseComponents) { 105 messageText = messageText.replace("\\" + KRADConstants.MessageParsing.LEFT_TOKEN, 106 KRADConstants.MessageParsing.LEFT_BRACKET); 107 messageText = messageText.replace("\\" + KRADConstants.MessageParsing.RIGHT_TOKEN, 108 KRADConstants.MessageParsing.RIGHT_BRACKET); 109 messageText = messageText.replace(KRADConstants.MessageParsing.RIGHT_TOKEN, 110 KRADConstants.MessageParsing.RIGHT_TOKEN_PLACEHOLDER); 111 String[] messagePieces = messageText.split("[\\" + KRADConstants.MessageParsing.LEFT_TOKEN + 112 "|\\" + KRADConstants.MessageParsing.RIGHT_TOKEN + "]"); 113 114 List<Component> messageComponentStructure = new ArrayList<Component>(); 115 116 //current message object to concatenate to after it is generated to prevent whitespace issues and 117 //creation of multiple unneeded objects 118 Message currentMessageComponent = null; 119 120 for (String messagePiece : messagePieces) { 121 if (messagePiece.endsWith(KRADConstants.MessageParsing.RIGHT_TOKEN_MARKER)) { 122 messagePiece = StringUtils.removeEnd(messagePiece, KRADConstants.MessageParsing.RIGHT_TOKEN_MARKER); 123 124 if (StringUtils.startsWithIgnoreCase(messagePiece, KRADConstants.MessageParsing.COMPONENT_BY_ID + "=") 125 && parseComponents) { 126 //Component by Id 127 currentMessageComponent = processIdComponentContent(messagePiece, messageComponentStructure, 128 currentMessageComponent, view); 129 } else if (messagePiece.matches("^[0-9]+( .+=.+)*$") && parseComponents) { 130 //Component by index of inlineComponents 131 currentMessageComponent = processIndexComponentContent(messagePiece, componentList, 132 messageComponentStructure, currentMessageComponent, view, messageId); 133 } else if (StringUtils.startsWithIgnoreCase(messagePiece, KRADConstants.MessageParsing.COLOR + "=") 134 || StringUtils.startsWithIgnoreCase(messagePiece, "/" + KRADConstants.MessageParsing.COLOR)) { 135 //Color span 136 currentMessageComponent = processColorContent(messagePiece, currentMessageComponent, view); 137 } else if (StringUtils.startsWithIgnoreCase(messagePiece, 138 KRADConstants.MessageParsing.CSS_CLASSES + "=") || StringUtils.startsWithIgnoreCase( 139 messagePiece, "/" + KRADConstants.MessageParsing.CSS_CLASSES)) { 140 //css class span 141 currentMessageComponent = processCssClassContent(messagePiece, currentMessageComponent, view); 142 } else if (StringUtils.startsWithIgnoreCase(messagePiece, KRADConstants.MessageParsing.LINK + "=") 143 || StringUtils.startsWithIgnoreCase(messagePiece, "/" + KRADConstants.MessageParsing.LINK)) { 144 //link (a tag) 145 currentMessageComponent = processLinkContent(messagePiece, currentMessageComponent, view); 146 } else if (StringUtils.startsWithIgnoreCase(messagePiece, 147 KRADConstants.MessageParsing.ACTION_LINK + "=") || StringUtils.startsWithIgnoreCase( 148 messagePiece, "/" + KRADConstants.MessageParsing.ACTION_LINK)) { 149 //action link (a tag) 150 currentMessageComponent = processActionLinkContent(messagePiece, currentMessageComponent, view); 151 } else if (messagePiece.equals("")) { 152 //do nothing 153 } else { 154 //raw html content 155 currentMessageComponent = processHtmlContent(messagePiece, currentMessageComponent, view); 156 } 157 } else { 158 //raw string 159 addBlanks(messagePiece); 160 currentMessageComponent = concatenateStringMessageContent(currentMessageComponent, messagePiece, view); 161 } 162 } 163 164 if (currentMessageComponent != null && StringUtils.isNotEmpty(currentMessageComponent.getMessageText())) { 165 messageComponentStructure.add(currentMessageComponent); 166 currentMessageComponent = null; 167 } 168 169 return messageComponentStructure; 170 } 171 172 /** 173 * Concatenates string content onto the message passed in and passes it back. If the message is null, creates 174 * a new message object with the string content and passes that back. 175 * 176 * @param currentMessageComponent Message object 177 * @param messagePiece string content to be concatenated 178 * @param view the current view 179 * @return resulting concatenated Message 180 */ 181 private static Message concatenateStringMessageContent(Message currentMessageComponent, String messagePiece, 182 View view) { 183 if (currentMessageComponent == null) { 184 currentMessageComponent = ComponentFactory.getMessage(); 185 186 if (view != null) { 187 view.assignComponentIds(currentMessageComponent); 188 } 189 190 currentMessageComponent.setMessageText(messagePiece); 191 currentMessageComponent.setGenerateSpan(false); 192 } else { 193 currentMessageComponent.setMessageText(currentMessageComponent.getMessageText() + messagePiece); 194 } 195 196 return currentMessageComponent; 197 } 198 199 /** 200 * Process the additional properties beyond index 0 of the tag (that was split into parts). 201 * 202 * <p>This will evaluate and set each of properties on the component passed in. This only allows 203 * setting of properties that can easily be converted to/from/are String type by Spring.</p> 204 * 205 * @param component component to have its properties set 206 * @param tagParts the tag split into parts, index 0 is ignored 207 * @return component with its properties set found in the tag's parts 208 */ 209 private static Component processAdditionalProperties(Component component, String[] tagParts) { 210 String componentString = tagParts[0]; 211 tagParts = (String[]) ArrayUtils.remove(tagParts, 0); 212 213 for (String part : tagParts) { 214 String[] propertyValue = part.split("="); 215 216 if (propertyValue.length == 2) { 217 String path = propertyValue[0]; 218 String value = propertyValue[1].trim(); 219 value = StringUtils.removeStart(value, "'"); 220 value = StringUtils.removeEnd(value, "'"); 221 ObjectPropertyUtils.setPropertyValue(component, path, value); 222 } else { 223 throw new RuntimeException( 224 "Invalid Message structure for component defined as " + componentString + " around " + part); 225 } 226 } 227 228 return component; 229 } 230 231 /** 232 * Inserts &nbsp; into the string passed in, if spaces exist at the beginning and/or end, 233 * so spacing is not lost in html translation. 234 * 235 * @param text string to insert &nbsp; 236 * @return String with &nbsp; inserted, if applicable 237 */ 238 public static String addBlanks(String text) { 239 if (StringUtils.startsWithIgnoreCase(text, " ")) { 240 text = " " + StringUtils.removeStart(text, " "); 241 } 242 243 if (text.endsWith(" ")) { 244 text = StringUtils.removeEnd(text, " ") + " "; 245 } 246 247 return text; 248 } 249 250 /** 251 * Process a piece of the message that has id content to get a component by id and insert it in the structure 252 * 253 * @param messagePiece String piece with component by id content 254 * @param messageComponentStructure the structure of the message being built 255 * @param currentMessageComponent the state of the current text based message being built 256 * @param view current View 257 * @return null if currentMessageComponent had a value (it is now added to the messageComponentStructure passed in) 258 */ 259 private static Message processIdComponentContent(String messagePiece, List<Component> messageComponentStructure, 260 Message currentMessageComponent, View view) { 261 //splits around spaces not included in single quotes 262 String[] parts = messagePiece.trim().trim().split("([ ]+(?=([^']*'[^']*')*[^']*$))"); 263 messagePiece = parts[0]; 264 265 //if there is a currentMessageComponent add it to the structure and reset it to null 266 //because component content is now interrupting the string content 267 if (currentMessageComponent != null && StringUtils.isNotEmpty(currentMessageComponent.getMessageText())) { 268 messageComponentStructure.add(currentMessageComponent); 269 currentMessageComponent = null; 270 } 271 272 //match component by id from the view 273 messagePiece = StringUtils.remove(messagePiece, "'"); 274 messagePiece = StringUtils.remove(messagePiece, "\""); 275 Component component = ComponentFactory.getNewComponentInstance(StringUtils.removeStart(messagePiece, 276 KRADConstants.MessageParsing.COMPONENT_BY_ID + "=")); 277 278 if (component != null) { 279 view.assignComponentIds(component); 280 component.addStyleClass(KRADConstants.MessageParsing.INLINE_COMP_CLASS); 281 282 if (parts.length > 1) { 283 component = processAdditionalProperties(component, parts); 284 } 285 messageComponentStructure.add(component); 286 } 287 288 return currentMessageComponent; 289 } 290 291 /** 292 * Process a piece of the message that has index content to get a component by index in the componentList passed in 293 * and insert it in the structure 294 * 295 * @param messagePiece String piece with component by index content 296 * @param componentList list that contains the component referenced by index 297 * @param messageComponentStructure the structure of the message being built 298 * @param currentMessageComponent the state of the current text based message being built 299 * @param view current View 300 * @param messageId id of the message being parsed (for exception notification) 301 * @return null if currentMessageComponent had a value (it is now added to the messageComponentStructure passed in) 302 */ 303 private static Message processIndexComponentContent(String messagePiece, List<Component> componentList, 304 List<Component> messageComponentStructure, Message currentMessageComponent, View view, String messageId) { 305 //splits around spaces not included in single quotes 306 String[] parts = messagePiece.trim().trim().split("([ ]+(?=([^']*'[^']*')*[^']*$))"); 307 messagePiece = parts[0]; 308 309 //if there is a currentMessageComponent add it to the structure and reset it to null 310 //because component content is now interrupting the string content 311 if (currentMessageComponent != null && StringUtils.isNotEmpty(currentMessageComponent.getMessageText())) { 312 messageComponentStructure.add(currentMessageComponent); 313 currentMessageComponent = null; 314 } 315 316 //match component by index from the componentList passed in 317 int cIndex = Integer.parseInt(messagePiece); 318 319 if (componentList != null && cIndex < componentList.size() && !componentList.isEmpty()) { 320 Component component = componentList.get(cIndex); 321 322 if (component != null) { 323 if (component.getId() == null) { 324 view.assignComponentIds(component); 325 } 326 327 if (parts.length > 1) { 328 component = processAdditionalProperties(component, parts); 329 } 330 331 component.addStyleClass(KRADConstants.MessageParsing.INLINE_COMP_CLASS); 332 messageComponentStructure.add(component); 333 } 334 } else { 335 throw new RuntimeException("Component with index " + cIndex + 336 " does not exist in inlineComponents of the message component with id " + messageId); 337 } 338 339 return currentMessageComponent; 340 } 341 342 /** 343 * Process a piece of the message that has color content by creating a span with that color style set 344 * 345 * @param messagePiece String piece with color content 346 * @param currentMessageComponent the state of the current text based message being built 347 * @param view current View 348 * @return currentMessageComponent with the new textual content generated by this method appended to its 349 * messageText 350 */ 351 private static Message processColorContent(String messagePiece, Message currentMessageComponent, View view) { 352 if (!StringUtils.startsWithIgnoreCase(messagePiece, "/")) { 353 messagePiece = StringUtils.remove(messagePiece, "'"); 354 messagePiece = StringUtils.remove(messagePiece, "\""); 355 messagePiece = "<span style='color: " + StringUtils.removeStart(messagePiece, 356 KRADConstants.MessageParsing.COLOR + "=") + ";'>"; 357 } else { 358 messagePiece = "</span>"; 359 } 360 361 return concatenateStringMessageContent(currentMessageComponent, messagePiece, view); 362 } 363 364 /** 365 * Process a piece of the message that has css content by creating a span with those css classes set 366 * 367 * @param messagePiece String piece with css class content 368 * @param currentMessageComponent the state of the current text based message being built 369 * @param view current View 370 * @return currentMessageComponent with the new textual content generated by this method appended to its 371 * messageText 372 */ 373 private static Message processCssClassContent(String messagePiece, Message currentMessageComponent, View view) { 374 if (!StringUtils.startsWithIgnoreCase(messagePiece, "/")) { 375 messagePiece = StringUtils.remove(messagePiece, "'"); 376 messagePiece = StringUtils.remove(messagePiece, "\""); 377 messagePiece = "<span class='" + StringUtils.removeStart(messagePiece, 378 KRADConstants.MessageParsing.CSS_CLASSES + "=") + "'>"; 379 } else { 380 messagePiece = "</span>"; 381 } 382 383 return concatenateStringMessageContent(currentMessageComponent, messagePiece, view); 384 } 385 386 /** 387 * Process a piece of the message that has link content by creating an anchor (a tag) with the href set 388 * 389 * @param messagePiece String piece with link content 390 * @param currentMessageComponent the state of the current text based message being built 391 * @param view current View 392 * @return currentMessageComponent with the new textual content generated by this method appended to its 393 * messageText 394 */ 395 private static Message processLinkContent(String messagePiece, Message currentMessageComponent, View view) { 396 if (!StringUtils.startsWithIgnoreCase(messagePiece, "/")) { 397 //clean up href 398 messagePiece = StringUtils.removeStart(messagePiece, KRADConstants.MessageParsing.LINK + "="); 399 messagePiece = StringUtils.removeStart(messagePiece, "'"); 400 messagePiece = StringUtils.removeEnd(messagePiece, "'"); 401 messagePiece = StringUtils.removeStart(messagePiece, "\""); 402 messagePiece = StringUtils.removeEnd(messagePiece, "\""); 403 404 messagePiece = "<a href='" + messagePiece + "' target='_blank'>"; 405 } else { 406 messagePiece = "</a>"; 407 } 408 409 return concatenateStringMessageContent(currentMessageComponent, messagePiece, view); 410 } 411 412 /** 413 * Process a piece of the message that has action link content by creating an anchor (a tag) with the onClick set 414 * to perform either ajaxSubmit or submit to the controller with a methodToCall 415 * 416 * @param messagePiece String piece with action link content 417 * @param currentMessageComponent the state of the current text based message being built 418 * @param view current View 419 * @return currentMessageComponent with the new textual content generated by this method appended to its 420 * messageText 421 */ 422 private static Message processActionLinkContent(String messagePiece, Message currentMessageComponent, View view) { 423 if (!StringUtils.startsWithIgnoreCase(messagePiece, "/")) { 424 messagePiece = StringUtils.removeStart(messagePiece, KRADConstants.MessageParsing.ACTION_LINK + "="); 425 String[] splitData = messagePiece.split(KRADConstants.MessageParsing.ACTION_DATA + "="); 426 427 String[] params = splitData[0].trim().split("([,]+(?=([^']*'[^']*')*[^']*$))"); 428 String methodToCall = ((params.length >= 1) ? params[0] : ""); 429 String validate = ((params.length >= 2) ? params[1] : "true"); 430 String ajaxSubmit = ((params.length >= 3) ? params[2] : "true"); 431 String successCallback = ((params.length >= 4) ? params[3] : "null"); 432 433 String submitData = "null"; 434 435 if (splitData.length > 1) { 436 submitData = splitData[1].trim(); 437 } 438 439 methodToCall = StringUtils.remove(methodToCall, "'"); 440 methodToCall = StringUtils.remove(methodToCall, "\""); 441 442 messagePiece = "<a href=\"javascript:void(null)\" onclick=\"submitForm(" + 443 "'" + 444 methodToCall + 445 "'," + 446 submitData + 447 "," + 448 validate + 449 "," + 450 ajaxSubmit + 451 "," + 452 successCallback + 453 "); return false;\">"; 454 } else { 455 messagePiece = "</a>"; 456 } 457 458 return concatenateStringMessageContent(currentMessageComponent, messagePiece, view); 459 } 460 461 /** 462 * Process a piece of the message that is assumed to have a valid html tag 463 * 464 * @param messagePiece String piece with html tag content 465 * @param currentMessageComponent the state of the current text based message being built 466 * @param view current View 467 * @return currentMessageComponent with the new textual content generated by this method appended to its 468 * messageText 469 */ 470 private static Message processHtmlContent(String messagePiece, Message currentMessageComponent, View view) { 471 //raw html 472 messagePiece = messagePiece.trim(); 473 474 if (StringUtils.startsWithAny(messagePiece, KRADConstants.MessageParsing.UNALLOWED_HTML) || StringUtils 475 .endsWithAny(messagePiece, KRADConstants.MessageParsing.UNALLOWED_HTML)) { 476 throw new RuntimeException("The following html is not allowed in Messages: " + Arrays.toString( 477 KRADConstants.MessageParsing.UNALLOWED_HTML)); 478 } 479 480 messagePiece = "<" + messagePiece + ">"; 481 482 return concatenateStringMessageContent(currentMessageComponent, messagePiece, view); 483 } 484 }