001/** 002 * Copyright 2005-2015 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.util; 017 018import org.apache.commons.lang.ArrayUtils; 019import org.apache.commons.lang.StringUtils; 020import org.kuali.rice.krad.uif.component.Component; 021import org.kuali.rice.krad.uif.element.Message; 022import org.kuali.rice.krad.uif.view.View; 023import org.kuali.rice.krad.util.KRADConstants; 024 025import java.util.ArrayList; 026import java.util.Arrays; 027import 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 */ 034public 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 messagePiece = 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 //messagePiece = messagePiece.indexOf(" "); 184/* if (messagePiece.startsWith(" ")){ 185 messagePiece.replaceFirst(" ", " "); 186 } 187 188 if (messagePiece.endsWith(" ")){ 189 190 }*/ 191 192 if (currentMessageComponent == null) { 193 currentMessageComponent = ComponentFactory.getMessage(); 194 195 if (view != null) { 196 view.assignComponentIds(currentMessageComponent); 197 } 198 199 currentMessageComponent.setMessageText(messagePiece); 200 currentMessageComponent.setGenerateSpan(false); 201 } else { 202 currentMessageComponent.setMessageText(currentMessageComponent.getMessageText() + messagePiece); 203 } 204 205 return currentMessageComponent; 206 } 207 208 /** 209 * Process the additional properties beyond index 0 of the tag (that was split into parts). 210 * 211 * <p>This will evaluate and set each of properties on the component passed in. This only allows 212 * setting of properties that can easily be converted to/from/are String type by Spring.</p> 213 * 214 * @param component component to have its properties set 215 * @param tagParts the tag split into parts, index 0 is ignored 216 * @return component with its properties set found in the tag's parts 217 */ 218 private static Component processAdditionalProperties(Component component, String[] tagParts) { 219 String componentString = tagParts[0]; 220 tagParts = (String[]) ArrayUtils.remove(tagParts, 0); 221 222 for (String part : tagParts) { 223 String[] propertyValue = part.split("="); 224 225 if (propertyValue.length == 2) { 226 String path = propertyValue[0]; 227 String value = propertyValue[1].trim(); 228 value = StringUtils.removeStart(value, "'"); 229 value = StringUtils.removeEnd(value, "'"); 230 ObjectPropertyUtils.setPropertyValue(component, path, value); 231 } else { 232 throw new RuntimeException( 233 "Invalid Message structure for component defined as " + componentString + " around " + part); 234 } 235 } 236 237 return component; 238 } 239 240 /** 241 * Inserts &nbsp; into the string passed in, if spaces exist at the beginning and/or end, 242 * so spacing is not lost in html translation. 243 * 244 * @param text string to insert &nbsp; 245 * @return String with &nbsp; inserted, if applicable 246 */ 247 public static String addBlanks(String text) { 248 if (StringUtils.startsWithIgnoreCase(text, " ")) { 249 text = " " + StringUtils.removeStart(text, " "); 250 } 251 252 if (text.endsWith(" ")) { 253 text = StringUtils.removeEnd(text, " ") + " "; 254 } 255 256 return text; 257 } 258 259 /** 260 * Process a piece of the message that has id content to get a component by id and insert it in the structure 261 * 262 * @param messagePiece String piece with component by id content 263 * @param messageComponentStructure the structure of the message being built 264 * @param currentMessageComponent the state of the current text based message being built 265 * @param view current View 266 * @return null if currentMessageComponent had a value (it is now added to the messageComponentStructure passed in) 267 */ 268 private static Message processIdComponentContent(String messagePiece, List<Component> messageComponentStructure, 269 Message currentMessageComponent, View view) { 270 //splits around spaces not included in single quotes 271 String[] parts = messagePiece.trim().trim().split("([ ]+(?=([^']*'[^']*')*[^']*$))"); 272 messagePiece = parts[0]; 273 274 //if there is a currentMessageComponent add it to the structure and reset it to null 275 //because component content is now interrupting the string content 276 if (currentMessageComponent != null && StringUtils.isNotEmpty(currentMessageComponent.getMessageText())) { 277 messageComponentStructure.add(currentMessageComponent); 278 currentMessageComponent = null; 279 } 280 281 //match component by id from the view 282 messagePiece = StringUtils.remove(messagePiece, "'"); 283 messagePiece = StringUtils.remove(messagePiece, "\""); 284 Component component = ComponentFactory.getNewComponentInstance(StringUtils.removeStart(messagePiece, 285 KRADConstants.MessageParsing.COMPONENT_BY_ID + "=")); 286 287 if (component != null) { 288 view.assignComponentIds(component); 289 component.addStyleClass(KRADConstants.MessageParsing.INLINE_COMP_CLASS); 290 291 if (parts.length > 1) { 292 component = processAdditionalProperties(component, parts); 293 } 294 messageComponentStructure.add(component); 295 } 296 297 return currentMessageComponent; 298 } 299 300 /** 301 * Process a piece of the message that has index content to get a component by index in the componentList passed in 302 * and insert it in the structure 303 * 304 * @param messagePiece String piece with component by index content 305 * @param componentList list that contains the component referenced by index 306 * @param messageComponentStructure the structure of the message being built 307 * @param currentMessageComponent the state of the current text based message being built 308 * @param view current View 309 * @param messageId id of the message being parsed (for exception notification) 310 * @return null if currentMessageComponent had a value (it is now added to the messageComponentStructure passed in) 311 */ 312 private static Message processIndexComponentContent(String messagePiece, List<Component> componentList, 313 List<Component> messageComponentStructure, Message currentMessageComponent, View view, String messageId) { 314 //splits around spaces not included in single quotes 315 String[] parts = messagePiece.trim().trim().split("([ ]+(?=([^']*'[^']*')*[^']*$))"); 316 messagePiece = parts[0]; 317 318 //if there is a currentMessageComponent add it to the structure and reset it to null 319 //because component content is now interrupting the string content 320 if (currentMessageComponent != null && StringUtils.isNotEmpty(currentMessageComponent.getMessageText())) { 321 messageComponentStructure.add(currentMessageComponent); 322 currentMessageComponent = null; 323 } 324 325 //match component by index from the componentList passed in 326 int cIndex = Integer.parseInt(messagePiece); 327 328 if (componentList != null && cIndex < componentList.size() && !componentList.isEmpty()) { 329 Component component = componentList.get(cIndex); 330 331 if (component != null) { 332 if (component.getId() == null) { 333 view.assignComponentIds(component); 334 } 335 336 if (parts.length > 1) { 337 component = processAdditionalProperties(component, parts); 338 } 339 340 component.addStyleClass(KRADConstants.MessageParsing.INLINE_COMP_CLASS); 341 messageComponentStructure.add(component); 342 } 343 } else { 344 throw new RuntimeException("Component with index " + cIndex + 345 " does not exist in inlineComponents of the message component with id " + messageId); 346 } 347 348 return currentMessageComponent; 349 } 350 351 /** 352 * Process a piece of the message that has color content by creating a span with that color style set 353 * 354 * @param messagePiece String piece with color content 355 * @param currentMessageComponent the state of the current text based message being built 356 * @param view current View 357 * @return currentMessageComponent with the new textual content generated by this method appended to its 358 * messageText 359 */ 360 private static Message processColorContent(String messagePiece, Message currentMessageComponent, View view) { 361 if (!StringUtils.startsWithIgnoreCase(messagePiece, "/")) { 362 messagePiece = StringUtils.remove(messagePiece, "'"); 363 messagePiece = StringUtils.remove(messagePiece, "\""); 364 messagePiece = "<span style='color: " + StringUtils.removeStart(messagePiece, 365 KRADConstants.MessageParsing.COLOR + "=") + ";'>"; 366 } else { 367 messagePiece = "</span>"; 368 } 369 370 return concatenateStringMessageContent(currentMessageComponent, messagePiece, view); 371 } 372 373 /** 374 * Process a piece of the message that has css content by creating a span with those css classes set 375 * 376 * @param messagePiece String piece with css class content 377 * @param currentMessageComponent the state of the current text based message being built 378 * @param view current View 379 * @return currentMessageComponent with the new textual content generated by this method appended to its 380 * messageText 381 */ 382 private static Message processCssClassContent(String messagePiece, Message currentMessageComponent, View view) { 383 if (!StringUtils.startsWithIgnoreCase(messagePiece, "/")) { 384 messagePiece = StringUtils.remove(messagePiece, "'"); 385 messagePiece = StringUtils.remove(messagePiece, "\""); 386 messagePiece = "<span class='" + StringUtils.removeStart(messagePiece, 387 KRADConstants.MessageParsing.CSS_CLASSES + "=") + "'>"; 388 } else { 389 messagePiece = "</span>"; 390 } 391 392 return concatenateStringMessageContent(currentMessageComponent, messagePiece, view); 393 } 394 395 /** 396 * Process a piece of the message that has link content by creating an anchor (a tag) with the href set 397 * 398 * @param messagePiece String piece with link content 399 * @param currentMessageComponent the state of the current text based message being built 400 * @param view current View 401 * @return currentMessageComponent with the new textual content generated by this method appended to its 402 * messageText 403 */ 404 private static Message processLinkContent(String messagePiece, Message currentMessageComponent, View view) { 405 if (!StringUtils.startsWithIgnoreCase(messagePiece, "/")) { 406 //clean up href 407 messagePiece = StringUtils.removeStart(messagePiece, KRADConstants.MessageParsing.LINK + "="); 408 messagePiece = StringUtils.removeStart(messagePiece, "'"); 409 messagePiece = StringUtils.removeEnd(messagePiece, "'"); 410 messagePiece = StringUtils.removeStart(messagePiece, "\""); 411 messagePiece = StringUtils.removeEnd(messagePiece, "\""); 412 413 messagePiece = "<a href='" + messagePiece + "' target='_blank'>"; 414 } else { 415 messagePiece = "</a>"; 416 } 417 418 return concatenateStringMessageContent(currentMessageComponent, messagePiece, view); 419 } 420 421 /** 422 * Process a piece of the message that has action link content by creating an anchor (a tag) with the onClick set 423 * to perform either ajaxSubmit or submit to the controller with a methodToCall 424 * 425 * @param messagePiece String piece with action link content 426 * @param currentMessageComponent the state of the current text based message being built 427 * @param view current View 428 * @return currentMessageComponent with the new textual content generated by this method appended to its 429 * messageText 430 */ 431 private static Message processActionLinkContent(String messagePiece, Message currentMessageComponent, View view) { 432 if (!StringUtils.startsWithIgnoreCase(messagePiece, "/")) { 433 messagePiece = StringUtils.removeStart(messagePiece, KRADConstants.MessageParsing.ACTION_LINK + "="); 434 String[] splitData = messagePiece.split(KRADConstants.MessageParsing.ACTION_DATA + "="); 435 436 String[] params = splitData[0].trim().split("([,]+(?=([^']*'[^']*')*[^']*$))"); 437 String methodToCall = ((params.length >= 1) ? params[0] : ""); 438 String validate = ((params.length >= 2) ? params[1] : "true"); 439 String ajaxSubmit = ((params.length >= 3) ? params[2] : "true"); 440 String successCallback = ((params.length >= 4) ? params[3] : "null"); 441 442 String submitData = "null"; 443 444 if (splitData.length > 1) { 445 submitData = splitData[1].trim(); 446 } 447 448 methodToCall = StringUtils.remove(methodToCall, "'"); 449 methodToCall = StringUtils.remove(methodToCall, "\""); 450 451 messagePiece = "<a href=\"javascript:void(null)\" onclick=\"submitForm(" + 452 "'" + 453 methodToCall + 454 "'," + 455 submitData + 456 "," + 457 validate + 458 "," + 459 ajaxSubmit + 460 "," + 461 successCallback + 462 "); return false;\">"; 463 } else { 464 messagePiece = "</a>"; 465 } 466 467 return concatenateStringMessageContent(currentMessageComponent, messagePiece, view); 468 } 469 470 /** 471 * Process a piece of the message that is assumed to have a valid html tag 472 * 473 * @param messagePiece String piece with html tag content 474 * @param currentMessageComponent the state of the current text based message being built 475 * @param view current View 476 * @return currentMessageComponent with the new textual content generated by this method appended to its 477 * messageText 478 */ 479 private static Message processHtmlContent(String messagePiece, Message currentMessageComponent, View view) { 480 //raw html 481 messagePiece = messagePiece.trim(); 482 483 if (StringUtils.startsWithAny(messagePiece, KRADConstants.MessageParsing.UNALLOWED_HTML) || StringUtils 484 .endsWithAny(messagePiece, KRADConstants.MessageParsing.UNALLOWED_HTML)) { 485 throw new RuntimeException("The following html is not allowed in Messages: " + Arrays.toString( 486 KRADConstants.MessageParsing.UNALLOWED_HTML)); 487 } 488 489 messagePiece = "<" + messagePiece + ">"; 490 491 return concatenateStringMessageContent(currentMessageComponent, messagePiece, view); 492 } 493}