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 &lt;&gt; are not part of the content, they specify placeholders):
066         * <ul>
067         * <li>[id=&lt;component id&gt;] - 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>[&lt;html tag&gt;][/&lt;html tag&gt;] - insert html content directly into the message content at that
070         * location,
071         * without the need to escape the &lt;&gt; characters in xml</li>
072         * <li>[color=&lt;html color code/name&gt;][/color] - wrap content in color tags to make text that color
073         * in the message</li>
074         * <li>[css=&lt;css classes&gt;][/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=&lt;href src&gt;][/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=&lt;href src&gt;][/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=&lt;action settings&gt; 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 &amp;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  &amp;nbsp;
236         * @return String with  &amp;nbsp; inserted, if applicable
237         */
238        public static String addBlanks(String text) {
239            if (StringUtils.startsWithIgnoreCase(text, " ")) {
240                text = "&nbsp;" + StringUtils.removeStart(text, " ");
241            }
242    
243            if (text.endsWith(" ")) {
244                text = StringUtils.removeEnd(text, " ") + "&nbsp;";
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    }