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