001/**
002 * Copyright 2005-2015 The Kuali Foundation
003 *
004 * Licensed under the Educational Community License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 * http://www.opensource.org/licenses/ecl2.php
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016package org.kuali.rice.krad.uif.util;
017
018import org.apache.commons.lang.ArrayUtils;
019import org.apache.commons.lang.StringUtils;
020import org.kuali.rice.krad.uif.component.Component;
021import org.kuali.rice.krad.uif.element.Message;
022import org.kuali.rice.krad.uif.view.View;
023import org.kuali.rice.krad.util.KRADConstants;
024
025import java.util.ArrayList;
026import java.util.Arrays;
027import 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 */
034public 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                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}