001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 *      http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.apache.wicket.markup.resolver;
018
019import java.util.HashMap;
020import java.util.Map;
021
022import org.apache.wicket.Application;
023import org.apache.wicket.Component;
024import org.apache.wicket.MarkupContainer;
025import org.apache.wicket.WicketRuntimeException;
026import org.apache.wicket.core.util.lang.PropertyResolver;
027import org.apache.wicket.markup.ComponentTag;
028import org.apache.wicket.markup.MarkupElement;
029import org.apache.wicket.markup.MarkupException;
030import org.apache.wicket.markup.MarkupStream;
031import org.apache.wicket.markup.WicketTag;
032import org.apache.wicket.markup.parser.XmlTag.TagType;
033import org.apache.wicket.model.Model;
034import org.apache.wicket.request.Response;
035import org.apache.wicket.response.StringResponse;
036import org.apache.wicket.util.string.Strings;
037import org.apache.wicket.util.string.interpolator.MapVariableInterpolator;
038import org.apache.wicket.util.value.IValueMap;
039import org.slf4j.Logger;
040import org.slf4j.LoggerFactory;
041
042/**
043 * This is a tag resolver which handles <wicket:message key="myKey">Default
044 * Text</wicket:message>. The resolver will replace the whole tag with the message found in
045 * the properties file associated with the Page.
046 * <p>
047 * You can also nest child components inside a wicket:message and then reference them from the
048 * properties file. For example in the html
049 * 
050 * <pre>
051 *     &lt;wicket:message key=&quot;myKey&quot;&gt;
052 *        This text will be replaced with text from the properties file.
053 *        &lt;span wicket:id=&quot;amount&quot;&gt;[amount]&lt;/span&gt;.
054 *        &lt;a wicket:id=&quot;link&quot;&gt;
055 *            &lt;wicket:message key=&quot;linkText&quot;/&gt;
056 *        &lt;/a&gt;
057 *     &lt;/wicket:message&gt;
058 * </pre>
059 * 
060 * Then in the properties file have a variable with a name that matches the wicket:id for each child
061 * component. The variables can be in any order, they do NOT have to match the order in the HTML
062 * file.
063 * 
064 * <pre>
065 *     myKey=Your balance is ${amount}. Click ${link} to view the details.
066 *     linkText=here
067 * </pre>
068 * 
069 * And in the java
070 * 
071 * <pre>
072 * add(new Label(&quot;amount&quot;, new Model&lt;String&gt;(&quot;$5.00&quot;)));
073 * add(new BookmarkablePageLink&lt;Void&gt;(&quot;link&quot;, DetailsPage.class));
074 * </pre>
075 * 
076 * This will output
077 * 
078 * <pre>
079 * Your balance is $5.00. Click &lt;a href=&quot;#&quot;&gt;here&lt;/a&gt; to view the details.
080 * </pre>
081 * 
082 * If variables are not found via child component, the search will continue with the parents
083 * container model object and if still not found with the parent container itself.
084 * 
085 * It is possible to switch between logging a warning and throwing an exception if either the
086 * property key/value or any of the variables can not be found.
087 *
088 * @see org.apache.wicket.settings.ResourceSettings#setThrowExceptionOnMissingResource(boolean)
089 * @author Juergen Donnerstag
090 * @author John Ray
091 */
092public class WicketMessageResolver implements IComponentResolver
093{
094        private static final long serialVersionUID = 1L;
095
096        private static final Logger log = LoggerFactory.getLogger(WicketMessageResolver.class);
097
098        /** */
099        public static final String MESSAGE = "message";
100
101        /**
102         * If the key can't be resolved and the default is null, an exception will be thrown. Instead,
103         * we default to a unique string and check against this later. Don't just use an empty string
104         * here, as people might want to override wicket:messages to empty strings.
105         */
106        private static final String DEFAULT_VALUE = "DEFAULT_WICKET_MESSAGE_RESOLVER_VALUE";
107
108        /**
109         * The name of the attribute that defines the resource key
110         */
111        public static final String KEY_ATTRIBUTE = "key";
112
113        /**
114         * The name of the attribute that defines whether the resource value should be HTML escaped
115         */
116        public static final String ESCAPE_ATTRIBUTE = "escape";
117
118        @Override
119        public Component resolve(final MarkupContainer container, final MarkupStream markupStream,
120                final ComponentTag tag)
121        {
122                if (tag instanceof WicketTag)
123                {
124                        WicketTag wtag = (WicketTag)tag;
125                        if (wtag.isMessageTag())
126                        {
127                                IValueMap attributes = wtag.getAttributes();
128                                String messageKey = attributes.getString(KEY_ATTRIBUTE);
129                                if (Strings.isEmpty(messageKey))
130                                {
131                                        throw new MarkupException(
132                                                "Wrong format of <wicket:message key='xxx'>: attribute 'key' is missing");
133                                }
134
135                                boolean escape = attributes.getBoolean(ESCAPE_ATTRIBUTE);
136
137                                final String id = wtag.getId();
138                                MessageContainer label = new MessageContainer(id, messageKey, escape);
139                                label.setRenderBodyOnly(container.getApplication()
140                                        .getMarkupSettings()
141                                        .getStripWicketTags());
142
143                                return label;
144                        }
145                }
146
147                // We were not able to handle the tag
148                return null;
149        }
150
151        /**
152         * If true, than throw an exception if a property key is not found. If false, just a warning is
153         * issued in the logged.
154         * 
155         * @return throwExceptionIfPropertyNotFound
156         */
157        private static boolean isThrowExceptionIfPropertyNotFound()
158        {
159                return Application.get().getResourceSettings().getThrowExceptionOnMissingResource();
160        }
161
162        /**
163         * A Container which expands open-close tags to open-body-close if required. It gets a
164         * properties value and replaces variable such as ${myVar} with the rendered output of its child
165         * tags.
166         * 
167         */
168        private static class MessageContainer extends MarkupContainer implements IComponentResolver
169        {
170                private static final long serialVersionUID = 1L;
171
172                private static final String NOT_FOUND = "[Warning: Property for '%s' not found]";
173
174                private final boolean escapeValue;
175
176                /**
177                 * Construct.
178                 * 
179                 * @param id
180                 * @param messageKey
181                 * @param escapeValue
182                 */
183                private MessageContainer(final String id, final String messageKey, boolean escapeValue)
184                {
185                        // The message key becomes the model
186                        super(id, new Model<>(messageKey));
187                        this.escapeValue = escapeValue;
188
189                        setEscapeModelStrings(false);
190                }
191
192                @Override
193                public Component resolve(MarkupContainer container, MarkupStream markupStream,
194                        ComponentTag tag)
195                {
196                        return getParent().get(tag.getId());
197                }
198
199                @Override
200                protected void onComponentTag(final ComponentTag tag)
201                {
202                        // Convert <wicket:message /> into <wicket:message>...</wicket:message>
203                        if (tag.isOpenClose())
204                        {
205                                tag.setType(TagType.OPEN);
206                        }
207                        super.onComponentTag(tag);
208                }
209
210                @Override
211                public void onComponentTagBody(final MarkupStream markupStream, final ComponentTag openTag)
212                {
213                        // Get the value from the properties file
214                        final String key = getDefaultModelObjectAsString();
215                        final String value = getLocalizer().getString(key, getParent(), DEFAULT_VALUE);
216
217                        // if found, than render it after replacing the variables
218                        if ((value != null) && !DEFAULT_VALUE.equals(value))
219                        {
220                                renderMessage(markupStream, openTag, key, value);
221                        }
222                        else
223                        {
224                                if (isThrowExceptionIfPropertyNotFound() == true)
225                                {
226                                        throw new WicketRuntimeException("Property '" + key +
227                                                "' not found in property files. Markup: " + markupStream.toString());
228                                }
229
230                                log.warn("No value found for wicket:message tag with key: {}", key);
231
232                                // If open tag was open-close
233                                if (markupStream.isCurrentIndexInsideTheStream() == false)
234                                {
235                                        String formatedNotFound = String.format(NOT_FOUND, key);
236                                        getResponse().write(formatedNotFound);
237                                }
238                                super.onComponentTagBody(markupStream, openTag);
239                        }
240                }
241
242                /**
243                 * A property key has been found. Now render the property value.
244                 * 
245                 * @param markupStream
246                 * @param openTag
247                 * @param key
248                 * @param value
249                 */
250                private void renderMessage(final MarkupStream markupStream, final ComponentTag openTag,
251                        final String key, final String value)
252                {
253                        // Find all direct child tags, render them separately into a String, and remember them
254                        // in a hash map associated with the wicket id
255                        final Map<String, CharSequence> childTags = findAndRenderChildWicketTags(markupStream,
256                                openTag);
257
258                        final Map<String, Object> variablesReplaced = new HashMap<String, Object>();
259
260                        // Replace all ${var} within the property value with real values
261                        CharSequence text = new MapVariableInterpolator(value, childTags)
262                        {
263                                @Override
264                                protected String getValue(final String variableName)
265                                {
266                                        // First check if a child tag with the same id exists.
267                                        String value = super.getValue(variableName);
268
269                                        // Remember that we successfully used the tag
270                                        if (value != null)
271                                        {
272                                                variablesReplaced.put(variableName, null);
273                                        }
274
275                                        // If not, try to resolve the name with containers model data
276                                        if (value == null)
277                                        {
278                                                value = Strings.toString(PropertyResolver.getValue(variableName,
279                                                        getParent().getDefaultModelObject()));
280                                        }
281
282                                        // If still not found, try the component itself
283                                        if (value == null)
284                                        {
285                                                value = Strings.toString(PropertyResolver.getValue(variableName,
286                                                        getParent()));
287                                        }
288
289                                        // If still not found, don't know what to do
290                                        if (value == null)
291                                        {
292                                                String msg = "The localized text for <wicket:message key=\"" + key +
293                                                        "\"> has a variable ${" + variableName +
294                                                        "}. However the wicket:message element does not have a child " +
295                                                        "element with a wicket:id=\"" + variableName + "\".";
296
297                                                if (isThrowExceptionIfPropertyNotFound() == true)
298                                                {
299                                                        markupStream.throwMarkupException(msg);
300                                                }
301                                                else
302                                                {
303                                                        log.warn(msg);
304                                                        value = "### VARIABLE NOT FOUND: " + variableName + " ###";
305                                                }
306                                        }
307
308                                        return value;
309                                }
310                        }.toString();
311
312                        if (escapeValue) 
313                        {
314                                text = Strings.escapeMarkup(text);
315                        }
316
317                        getResponse().write(text);
318
319                        // Make sure all of the children were rendered
320                        for (String id : childTags.keySet())
321                        {
322                                if (variablesReplaced.containsKey(id) == false)
323                                {
324                                        String msg = "The <wicket:message key=\"" + key +
325                                                "\"> has a child element with wicket:id=\"" + id +
326                                                "\". You must add the variable ${" + id +
327                                                "} to the localized text for the wicket:message.";
328
329                                        if (isThrowExceptionIfPropertyNotFound() == true)
330                                        {
331                                                markupStream.throwMarkupException(msg);
332                                        }
333                                        else
334                                        {
335                                                log.warn(msg);
336                                        }
337                                }
338                        }
339                }
340
341                /**
342                 * If the tag is of form <wicket:message>{foo}</wicket:message> then scan for any child
343                 * wicket component and save their tag index
344                 * 
345                 * @param markupStream
346                 * @param openTag
347                 * @return map of child components
348                 */
349                private Map<String, CharSequence> findAndRenderChildWicketTags(
350                        final MarkupStream markupStream, final ComponentTag openTag)
351                {
352                        Map<String, CharSequence> childTags = new HashMap<String, CharSequence>();
353
354                        // get original tag from markup because we modified openTag to always be open
355                        ComponentTag tag = markupStream.getPreviousTag();
356
357                        // if the tag is of form <wicket:message>{foo}</wicket:message> then scan for any
358                        // child component and save their tag index
359                        if (!tag.isOpenClose())
360                        {
361                                while (markupStream.isCurrentIndexInsideTheStream() && !markupStream.get().closes(openTag))
362                                {
363                                        MarkupElement element = markupStream.get();
364
365                                        // If it a tag like <wicket..> or <span wicket:id="..." >
366                                        if ((element instanceof ComponentTag) && !markupStream.atCloseTag())
367                                        {
368                                                ComponentTag currentTag = (ComponentTag)element;
369                                                String id = currentTag.getId();
370
371                                                // Temporarily replace the web response with a String response
372                                                final Response webResponse = getResponse();
373
374                                                try
375                                                {
376                                                        final StringResponse response = new StringResponse();
377                                                        getRequestCycle().setResponse(response);
378
379                                                        Component component = getParent().get(id);
380                                                        if (component == null)
381                                                        {
382                                                                component = ComponentResolvers.resolve(getParent(), markupStream,
383                                                                        currentTag, null);
384
385                                                                // Must not be a Page and it must be connected to a parent.
386                                                                if (component.getParent() == null)
387                                                                {
388                                                                        component = null;
389                                                                }
390                                                        }
391
392                                                        if (component != null)
393                                                        {
394                                                                component.render();
395                                                                markupStream.skipComponent();
396                                                        }
397                                                        else
398                                                        {
399                                                                markupStream.next();
400                                                        }
401                                                        childTags.put(id, response.getBuffer());
402                                                }
403                                                finally
404                                                {
405                                                        // Restore the original response
406                                                        getRequestCycle().setResponse(webResponse);
407                                                }
408                                        }
409                                        else
410                                        {
411                                                markupStream.next();
412                                        }
413                                }
414                        }
415
416                        return childTags;
417                }
418        }
419}