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 }