View Javadoc

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