001/**
002 * Copyright 2005-2018 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.element;
017
018import org.apache.commons.lang.StringUtils;
019import org.kuali.rice.krad.datadictionary.parse.BeanTag;
020import org.kuali.rice.krad.datadictionary.parse.BeanTagAttribute;
021import org.kuali.rice.krad.datadictionary.parse.BeanTags;
022import org.kuali.rice.krad.datadictionary.validator.ValidationTrace;
023import org.kuali.rice.krad.datadictionary.validator.Validator;
024import org.kuali.rice.krad.uif.UifConstants;
025import org.kuali.rice.krad.uif.component.Component;
026import org.kuali.rice.krad.uif.lifecycle.ViewLifecycle;
027import org.kuali.rice.krad.uif.lifecycle.ViewLifecycleRestriction;
028import org.kuali.rice.krad.uif.util.ComponentUtils;
029import org.kuali.rice.krad.uif.util.LifecycleElement;
030import org.kuali.rice.krad.uif.util.MessageStructureUtils;
031import org.kuali.rice.krad.util.KRADConstants;
032
033import java.util.List;
034import java.util.regex.Matcher;
035import java.util.regex.Pattern;
036
037/**
038 * Encapsulates a text message to be displayed
039 *
040 * <p>
041 * The <code>Message</code> is used to display static text in the user interface
042 * </p>
043 *
044 * @author Kuali Rice Team (rice.collab@kuali.org)
045 */
046@BeanTag(name = "message", parent = "Uif-Message")
047public class Message extends ContentElementBase {
048    private static final long serialVersionUID = 4090058533452450395L;
049
050    // This regex is a check to see if the message is a rich message and it contains potential non-inline elements
051    private static Pattern blockElementCheck = Pattern.compile(
052            "[\\[|\\<](?!color|action|link|css|button|input|label|select|textarea|abbr"
053                    + "|strong|img|a[\\s\\]]|span[\\s\\]]|b[\\s\\]]|i[\\s\\]]|br[\\s\\]/])[^/]*?/?[\\]|\\>]");
054
055    private String messageText;
056    private String wrapperTag;
057    private boolean renderWrapperTag;
058
059    private List<Component> inlineComponents;
060    private List<Component> messageComponentStructure;
061
062    private boolean parseComponents;
063    private boolean richMessage;
064    private boolean containsBlockElements;
065
066    public Message() {
067        super();
068
069        renderWrapperTag = true;
070        parseComponents = true;
071    }
072
073    /**
074     * Message perform apply model parses message text for rich text functionality if the messageText contains
075     * [ or ] special characters
076     *
077     * {@inheritDoc}
078     */
079    @Override
080    public void performApplyModel(Object model, LifecycleElement parent) {
081        super.performApplyModel(model, parent);
082
083        //if messageText contains the special characters [] then parse and fill in the messageComponentStructure
084        //but if messageComponentStructure has already been set it overrides messageText by default
085        if (messageText != null && messageText.contains(KRADConstants.MessageParsing.LEFT_TOKEN) &&
086                messageText.contains(KRADConstants.MessageParsing.RIGHT_TOKEN) &&
087                (messageComponentStructure == null || messageComponentStructure.isEmpty())) {
088            richMessage = true;
089
090            // Check to see if message contains potential block elements (non-inline)
091            Matcher matcher = blockElementCheck.matcher(messageText);
092            containsBlockElements = matcher.find();
093
094            if (StringUtils.isBlank(wrapperTag) && containsBlockElements) {
095                wrapperTag = UifConstants.WrapperTags.DIV;
096            }
097
098            messageComponentStructure = MessageStructureUtils.parseMessage(this.getId(), this.getMessageText(),
099                    this.getInlineComponents(), ViewLifecycle.getView(), parseComponents);
100        } else if (messageText != null && messageText.contains("<") && messageText.contains(">")) {
101            // Straight inline html case
102            // Check to see if message contains potential block elements (non-inline)
103            Matcher matcher = blockElementCheck.matcher(messageText);
104            containsBlockElements = matcher.find();
105
106            // Must be in a div it contains potential block elements
107            if (StringUtils.isBlank(wrapperTag) && containsBlockElements) {
108                wrapperTag = UifConstants.WrapperTags.DIV;
109            }
110        }
111
112        // If the wrapper element is not set by the bean def or the above logic to check for block elements, default
113        // to the p tag
114        if (StringUtils.isBlank(wrapperTag)) {
115            wrapperTag = UifConstants.WrapperTags.P;
116        }
117    }
118
119    /**
120     * {@inheritDoc}
121     */
122    @Override
123    public void performFinalize(Object model, LifecycleElement parent) {
124        super.performFinalize(model, parent);
125
126        if (messageComponentStructure != null && !messageComponentStructure.isEmpty()) {
127            // Message needs to be aware of its own parent because it now contains content that can have validation
128            this.addDataAttribute(UifConstants.DataAttributes.PARENT, parent.getId());
129        }
130    }
131
132    /**
133     * Override to render only if the message text has been given or there is a conditional expression on the
134     * message text
135     *
136     * {@inheritDoc}
137     */
138    @Override
139    public boolean isRender() {
140        boolean render = super.isRender();
141
142        if (render) {
143            render = getPropertyExpressions().containsKey("messageText") || (StringUtils.isNotBlank(messageText)
144                    && !StringUtils.equals(messageText, "&nbsp;"));
145        }
146
147        return render;
148    }
149
150    /**
151     * Text that makes up the message that will be displayed.
152     *
153     * <p>If special characters [] are detected the message is split at that location.  The types of features supported
154     * by the parse are (note that &lt;&gt; are not part of the content, they specify placeholders):
155     * <ul>
156     * <li>[id=&lt;component id&gt;] - insert component with id specified at that location in the message</li>
157     * <li>[n] - insert component at index n from the inlineComponent list</li>
158     * <li>[&lt;html tag&gt;][/&lt;html tag&gt;] - insert html content directly into the message content at that
159     * location,
160     * without the need to escape the &lt;&gt; characters in xml</li>
161     * <li>[color=&lt;html color code/name&gt;][/color] - wrap content in color tags to make text that color
162     * in the message</li>
163     * <li>[css=&lt;css classes&gt;][/css] - apply css classes specified to the wrapped content - same as wrapping
164     * the content in span with class property set</li>
165     * <li>[link=&lt;href src&gt;][/link] - an easier way to create an anchor that will open in a new page to the
166     * href specified after =</li>
167     * <li>[action=&lt;href src&gt;][/action] - create an action link inline without having to specify a component by
168     * id or index.  The options for this are as follows and MUST be in a comma separated list in the order specified
169     * (specify 1-4 always in this order):
170     * <ul>
171     * <li>methodToCall(String)</li>
172     * <li>validateClientSide(boolean) - true if not set</li>
173     * <li>ajaxSubmit(boolean) - true if not set</li>
174     * <li>successCallback(js function or function declaration) - this only works when ajaxSubmit is true</li>
175     * </ul>
176     * The tag would look something like this [action=methodToCall]Action[/action] in most common cases.  And in more
177     * complex cases [action=methodToCall,true,true,functionName]Action[/action].  <p>In addition to these settings,
178     * you can also specify data to send to the server in this fashion (space is required between settings and data):
179     * </p>
180     * [action=&lt;action settings&gt; data={key1: 'value 1', key2: value2}]
181     * </li>
182     * </ul>
183     * If the [] characters are needed in message text, they need to be declared with an escape character: \\[ \\]
184     * </p>
185     *
186     * @return message text
187     */
188    @BeanTagAttribute
189    public String getMessageText() {
190        return this.messageText;
191    }
192
193    /**
194     * Setter for the message text
195     *
196     * @param messageText
197     */
198    public void setMessageText(String messageText) {
199        this.messageText = messageText;
200    }
201
202    /**
203     * Defines the html tag that will wrap this message, if left blank, this will automatically be set by the framework
204     * to the appropriate tag (in most cases p or div)
205     *
206     * @return the html tag used to wrap this message
207     */
208    @BeanTagAttribute
209    public String getWrapperTag() {
210        return wrapperTag;
211    }
212
213    /**
214     * @see org.kuali.rice.krad.uif.element.Message#getWrapperTag()
215     */
216    public void setWrapperTag(String wrapperTag) {
217        this.wrapperTag = wrapperTag;
218    }
219
220    /**
221     * If true, render the wrapper element (p or div) around this message (default true).
222     *
223     * <p>The wrapper will be either a p tag, for when the element only contains inline elements, or a div tag, for
224     * when the message might contain block level elements or undetermined html tags resulting from rich message
225     * functionality.  When false, skips the wrapper generation for this
226     * message - this has the additional effect the css classes/style classes will be lost for this message. </p>
227     *
228     * @return true if generating a wrapping span, false otherwise
229     */
230    @BeanTagAttribute
231    public boolean isRenderWrapperTag() {
232        return renderWrapperTag && wrapperTag != null;
233    }
234
235    /**
236     * Sets the generate wrapper element flag
237     *
238     * @param renderWrapperTag
239     */
240    public void setRenderWrapperTag(boolean renderWrapperTag) {
241        this.renderWrapperTag = renderWrapperTag;
242    }
243
244    /**
245     * The message component structure is a list of components which represent the components that make up a message
246     * when using rich message functionality.
247     *
248     * <p>The structure represents the parsed messageText when not set. Normally this structure is setup by the Message
249     * class and <b>SHOULD NOT BE SET</b> in xml, unless full control over the structure is needed.</p>
250     *
251     * @return list of components which represent the message structure
252     */
253    public List<Component> getMessageComponentStructure() {
254        return messageComponentStructure;
255    }
256
257    /**
258     * Set the message component structure.  This will override/ignore messageText when set. Normally
259     * this <b>SHOULD NOT BE SET</b> by the xml configuration.
260     *
261     * @param messageComponentStructure list of components which represent the message structure
262     */
263    public void setMessageComponentStructure(List<Component> messageComponentStructure) {
264        this.messageComponentStructure = messageComponentStructure;
265    }
266
267    /**
268     * The inlineComponents are a list of components in order by index.
269     *
270     * <p>inlineComponents is only used when the message is using rich message functionality.  A message
271     * with [0] will reference component at index 0 of this list and insert it at that place in the message,
272     * and likewise [1] will reference item 1, etc.  If the index referenced is out of bounds (or list doesn't exist),
273     * an error will be thrown during message parse.</p>
274     *
275     * @return the inlineComponents to be filled in at indexes referenced by [n] in the message
276     */
277    @BeanTagAttribute
278    @ViewLifecycleRestriction(exclude = { UifConstants.ViewPhases.APPLY_MODEL, UifConstants.ViewPhases.FINALIZE })
279    public List<Component> getInlineComponents() {
280        return inlineComponents;
281    }
282
283    /**
284     * Set the inlineComponents to be filled in at indexes referenced by [n] in the message
285     *
286     * @param inlineComponents the inlineComponents to be filled in at indexes referenced by [n] in the message
287     */
288    public void setInlineComponents(List<Component> inlineComponents) {
289        this.inlineComponents = inlineComponents;
290    }
291
292    /**
293     * Indicates if the inline components must be parsed for rich messages
294     *
295     * @return boolean
296     */
297    @BeanTagAttribute
298    public boolean isParseComponents() {
299        return parseComponents;
300    }
301
302    /**
303     * Sets the parse components flag to indicate if inline components must be parsed for rich messages
304     *
305     * @param parseComponents
306     */
307    public void setParseComponents(boolean parseComponents) {
308        this.parseComponents = parseComponents;
309    }
310
311    /**
312     * If this message is considered a rich message (is using some rich message functionality with by using
313     * the special [] tags), returns true
314     *
315     * @return return true if this message contains rich message content
316     */
317    public boolean isRichMessage() {
318        return richMessage;
319    }
320
321    /**
322     * True if the message contains block elements, or when it contains an unknown tag that may be a block element.
323     *
324     * @return true when the message contains block elements (non-inline elements), false otherwise
325     */
326    public boolean isContainsBlockElements() {
327        return containsBlockElements;
328    }
329
330    /**
331     * {@inheritDoc}
332     */
333    @Override
334    public void completeValidation(ValidationTrace tracer) {
335        tracer.addBean(this);
336
337        // Checks that text is set
338        if (getMessageText() == null) {
339            if (Validator.checkExpressions(this, "messageText")) {
340                String currentValues[] = {"messageText  =" + getMessageText()};
341                tracer.createWarning("MessageText should be set", currentValues);
342            }
343        }
344
345        super.completeValidation(tracer.getCopy());
346    }
347}