View Javadoc
1   /**
2    * Copyright 2005-2014 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.util;
17  
18  import org.apache.commons.lang.ArrayUtils;
19  import org.apache.commons.lang.StringUtils;
20  import org.kuali.rice.krad.uif.component.Component;
21  import org.kuali.rice.krad.uif.element.Message;
22  import org.kuali.rice.krad.uif.lifecycle.ViewLifecycle;
23  import org.kuali.rice.krad.uif.lifecycle.ViewPostMetadata;
24  import org.kuali.rice.krad.uif.view.View;
25  import org.kuali.rice.krad.util.KRADConstants;
26  
27  import java.util.ArrayList;
28  import java.util.Arrays;
29  import java.util.List;
30  
31  /**
32   * Rich message structure utilities for parsing message content and converting it to components/content
33   *
34   * @author Kuali Rice Team (rice.collab@kuali.org)
35   */
36  public class MessageStructureUtils {
37  
38      /**
39       * Translate a message with special hooks described in MessageStructureUtils.parseMessage.  However, tags which
40       * reference components will not be allowed/translated - only tags which can translate to string content will
41       * be included for this translation.
42       *
43       * @param messageText messageText with only String translateable tags included (no id or component index tags)
44       * @return html translation of rich messageText passed in
45       * @see MessageStructureUtils#parseMessage
46       */
47      public static String translateStringMessage(String messageText) {
48          if (!StringUtils.isEmpty(messageText)) {
49              List<Component> components = MessageStructureUtils.parseMessage(null, messageText, null, null, false);
50  
51              if (!components.isEmpty()) {
52                  Component message = components.get(0);
53  
54                  if (message instanceof Message) {
55                      messageText = ((Message) message).getMessageText();
56                  }
57              }
58          }
59  
60          return messageText;
61      }
62  
63      /**
64       * Parses the message text passed in and returns the resulting rich message component structure.
65       *
66       * <p>If special characters [] are detected the message is split at that location.  The types of features supported
67       * by the parse are (note that &lt;&gt; are not part of the content, they specify placeholders):
68       * <ul>
69       * <li>[id=&lt;component id&gt;] - insert component with id specified at that location in the message</li>
70       * <li>[n] - insert component at index n from the inlineComponent list</li>
71       * <li>[&lt;html tag&gt;][/&lt;html tag&gt;] - insert html content directly into the message content at that
72       * location,
73       * without the need to escape the &lt;&gt; characters in xml</li>
74       * <li>[color=&lt;html color code/name&gt;][/color] - wrap content in color tags to make text that color
75       * in the message</li>
76       * <li>[css=&lt;css classes&gt;][/css] - apply css classes specified to the wrapped content - same as wrapping
77       * the content in span with class property set</li>
78       * <li>[link=&lt;href src&gt;][/link] - an easier way to create an anchor that will open in a new page to the
79       * href specified after =</li>
80       * <li>[action=&lt;href src&gt;][/action] - create an action link inline without having to specify a component by
81       * id or index.  The options for this are as follows and MUST be in a comma seperated list in the order specified
82       * (specify 1-4 always in this order):
83       * <ul>
84       * <li>methodToCall(String)</li>
85       * <li>validateClientSide(boolean) - true if not set</li>
86       * <li>ajaxSubmit(boolean) - true if not set</li>
87       * <li>successCallback(js function or function declaration) - this only works when ajaxSubmit is true</li>
88       * </ul>
89       * The tag would look something like this [action=methodToCall]Action[/action] in most common cases.  And in more
90       * complex cases [action=methodToCall,true,true,functionName]Action[/action].  <p>In addition to these settings,
91       * you can also specify data to send to the server in this fashion (space is required between settings and data):
92       * </p>
93       * [action=&lt;action settings&gt; data={key1: 'value 1', key2: value2}]
94       * </li>
95       * </ul>
96       * If the [] characters are needed in message text, they need to be declared with an escape character: \\[ \\]
97       * </p>
98       *
99       * @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 &amp;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  &amp;nbsp;
234      * @return String with  &amp;nbsp; inserted, if applicable
235      */
236     public static String addBlanks(String text) {
237         if (StringUtils.startsWithIgnoreCase(text, " ")) {
238             text = "&nbsp;" + StringUtils.removeStart(text, " ");
239         }
240 
241         if (text.endsWith(" ")) {
242             text = StringUtils.removeEnd(text, " ") + "&nbsp;";
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 }