/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.wicket.markup.resolver;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import org.apache.wicket.Application;
import org.apache.wicket.Component;
import org.apache.wicket.MarkupContainer;
import org.apache.wicket.Response;
import org.apache.wicket.WicketRuntimeException;
import org.apache.wicket.markup.ComponentTag;
import org.apache.wicket.markup.MarkupElement;
import org.apache.wicket.markup.MarkupException;
import org.apache.wicket.markup.MarkupStream;
import org.apache.wicket.markup.WicketTag;
import org.apache.wicket.markup.parser.XmlTag;
import org.apache.wicket.markup.parser.filter.WicketTagIdentifier;
import org.apache.wicket.model.Model;
import org.apache.wicket.response.StringResponse;
import org.apache.wicket.util.lang.PropertyResolver;
import org.apache.wicket.util.string.Strings;
import org.apache.wicket.util.string.interpolator.MapVariableInterpolator;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* This is a tag resolver which handles <wicket:message key="myKey">Default
* Text</wicket:message>. The resolver will replace the whole tag with the message found in
* the properties file associated with the Page. If no message is found, the default body text will
* remain.
* <p>
* You can also nest child components inside a wicket:message and then reference them from the
* properties file. For example in the html
*
* <pre>
* <wicket:message key="myKey">
* This text will be replaced with text from the properties file.
* <span wicket:id="amount">[amount]</span>.
* <a wicket:id="link">
* <wicket:message key="linkText"/>
* </a>
* </wicket:message>
* </pre>
*
* Then in the properties file have a variable with a name that matches the wicket:id for each child
* component. The variables can be in any order, they do NOT have to match the order in the HTML
* file.
*
* <pre>
* myKey=Your balance is ${amount}. Click ${link} to view the details.
* linkText=here
* </pre>
*
* And in the java
*
* <pre>
* add(new Label("amount", new Model<String>("$5.00")));
* add(new BookmarkablePageLink("link", DetailsPage.class));
* </pre>
*
* This will output
*
* <pre>
* Your balance is $5.00. Click <a href="#">here</a> to view the details.
* </pre>
*
* If variables are not found via child component, the search will continue with the parents
* container model object and if still not found with the parent container itself.
*
* It is possible to switch between logging a warning and throwing an exception if either the
* property key/value or any of the variables can not be found.
*
* @author Juergen Donnerstag
* @author John Ray
*/
public class WicketMessageResolver implements IComponentResolver
{
private static final long serialVersionUID = 1L;
private static final Logger log = LoggerFactory.getLogger(WicketMessageResolver.class);
static
{
// register "wicket:message"
WicketTagIdentifier.registerWellKnownTagName("message");
}
/**
* If the key can't be resolved and the default is null, an exception will be thrown. Instead,
* we default to a unique string and check against this later. Don't just use an empty string
* here, as people might want to override wicket:messages to empty strings.
*/
private static final String DEFAULT_VALUE = "DEFAULT_WICKET_MESSAGE_RESOLVER_VALUE";
/**
* Try to resolve the tag, then create a component, add it to the container and render it.
*
* @see org.apache.wicket.markup.resolver.IComponentResolver#resolve(MarkupContainer,
* MarkupStream, ComponentTag)
*
* @param container
* The container parsing its markup
* @param markupStream
* The current markupStream
* @param tag
* The current component tag while parsing the markup
* @return true, if componentId was handle by the resolver. False, otherwise
*/
public boolean resolve(final MarkupContainer container, final MarkupStream markupStream,
final ComponentTag tag)
{
if (tag instanceof WicketTag)
{
WicketTag wtag = (WicketTag)tag;
if (wtag.isMessageTag())
{
String messageKey = wtag.getAttributes().getString("key");
if ((messageKey == null) || (messageKey.trim().length() == 0))
{
throw new MarkupException(
"Wrong format of <wicket:message key='xxx'>: attribute 'key' is missing");
}
final String id = "_message_" + container.getPage().getAutoIndex();
MessageContainer label = new MessageContainer(id, messageKey);
label.setRenderBodyOnly(container.getApplication()
.getMarkupSettings()
.getStripWicketTags());
container.autoAdd(label, markupStream);
// Yes, we handled the tag
return true;
}
}
// We were not able to handle the tag
return false;
}
/**
* If true, than throw an exception if a property key is not found. If false, just a warning is
* issued in the logged.
*
* @return throwExceptionIfPropertyNotFound
*/
private static boolean isThrowExceptionIfPropertyNotFound()
{
return Application.get().getResourceSettings().getThrowExceptionOnMissingResource();
}
/**
* A Container which expands open-close tags to open-body-close if required. It gets a
* properties value and replaces variable such as ${myVar} with the rendered output of its child
* tags.
*
*/
private static class MessageContainer extends MarkupContainer
{
private static final long serialVersionUID = 1L;
/**
* Construct.
*
* @param id
* @param messageKey
*/
public MessageContainer(final String id, final String messageKey)
{
// The message key becomes the model
super(id, new Model<String>(messageKey));
setEscapeModelStrings(false);
}
/**
*
* @see org.apache.wicket.MarkupContainer#onComponentTagBody(org.apache.wicket.markup.MarkupStream,
* org.apache.wicket.markup.ComponentTag)
*/
@Override
protected void onComponentTagBody(final MarkupStream markupStream,
final ComponentTag openTag)
{
// Get the value from the properties file
final String key = getDefaultModelObjectAsString();
final String value = getLocalizer().getString(key, getParent(), DEFAULT_VALUE);
// if found, than render it after replacing the variables
if ((value != null) && !DEFAULT_VALUE.equals(value))
{
renderMessage(markupStream, openTag, key, value);
}
else
{
// TODO Doesn't localizer already throw an exception?!?!
if (isThrowExceptionIfPropertyNotFound() == true)
{
throw new WicketRuntimeException("Property '" + key +
"' not found in property files. Markup: " + markupStream.toString());
}
log.warn("No value found for wicket:message tag with key: {}", key);
renderComponentTagBody(markupStream, openTag);
}
}
/**
* A property key has been found. Now render the property value.
*
* @param markupStream
* @param openTag
* @param key
* @param value
*/
private void renderMessage(final MarkupStream markupStream, final ComponentTag openTag,
final String key, final String value)
{
// Find all direct child tags, render them separately into a String, and remember them
// in a hash map associated with the wicket id
final Map<String, CharSequence> childTags = findAndRenderChildWicketTags(markupStream,
openTag);
final Map<String, Object> variablesReplaced = new HashMap<String, Object>();
// Replace all ${var} within the property value with real values
String text = new MapVariableInterpolator(value, childTags)
{
/**
* @see org.apache.wicket.util.string.interpolator.MapVariableInterpolator#getValue(java.lang.String)
*/
@Override
protected String getValue(final String variableName)
{
// First check if a child tag with the same id exists.
String value = super.getValue(variableName);
// Remember that we successfully used the tag
if (value != null)
{
variablesReplaced.put(variableName, null);
}
// If not, try to resolve the name with containers model data
if (value == null)
{
value = Strings.toString(PropertyResolver.getValue(variableName,
getParent().getDefaultModelObject()));
}
// If still not found, try the component itself
if (value == null)
{
value = Strings.toString(PropertyResolver.getValue(variableName,
getParent()));
}
// If still not found, don't know what to do
if (value == null)
{
String msg = "The localized text for <wicket:message key=\"" + key +
"\"> has a variable ${" + variableName +
"}. However the wicket:message element does not have a child " +
"element with a wicket:id=\"" + variableName + "\".";
if (isThrowExceptionIfPropertyNotFound() == true)
{
markupStream.throwMarkupException(msg);
}
else
{
log.warn(msg);
value = "### VARIABLE NOT FOUND: " + variableName + " ###";
}
}
return value;
}
}.toString();
getResponse().write(text);
// Make sure all of the children were rendered
Iterator<String> iter = childTags.keySet().iterator();
while (iter.hasNext())
{
String id = iter.next();
if (variablesReplaced.containsKey(id) == false)
{
String msg = "The <wicket:message key=\"" + key +
"\"> has a child element with wicket:id=\"" + id +
"\". You must add the variable ${" + id +
"} to the localized text for the wicket:message.";
if (isThrowExceptionIfPropertyNotFound() == true)
{
markupStream.throwMarkupException(msg);
}
else
{
log.warn(msg);
}
}
}
}
/**
* If the tag is of form <wicket:message>{foo}</wicket:message> then scan for any child
* wicket component and save their tag index
*
* @param markupStream
* @param openTag
* @return map of child components
*/
private Map<String, CharSequence> findAndRenderChildWicketTags(
final MarkupStream markupStream, final ComponentTag openTag)
{
Map<String, CharSequence> childTags = new HashMap<String, CharSequence>();
// get original tag from markup because we modified openTag to always be open
markupStream.setCurrentIndex(markupStream.getCurrentIndex() - 1);
ComponentTag tag = markupStream.getTag();
markupStream.next();
// if the tag is of form <wicket:message>{foo}</wicket:message> then scan for any
// child component and save their tag index
if (!tag.isOpenClose())
{
while (markupStream.hasMore() && !markupStream.get().closes(openTag))
{
MarkupElement element = markupStream.get();
// If it a tag like <wicket..> or <span wicket:id="..." >
if ((element instanceof ComponentTag) && !markupStream.atCloseTag())
{
String id = ((ComponentTag)element).getId();
// Temporarily replace the web response with a String response
final Response webResponse = getResponse();
try
{
final StringResponse response = new StringResponse();
getRequestCycle().setResponse(response);
Component component = getParent().get(id);
if (component != null)
{
component.render(markupStream);
}
childTags.put(id, response.getBuffer());
}
finally
{
// Restore the original response
getRequestCycle().setResponse(webResponse);
}
}
else
{
markupStream.next();
}
}
}
return childTags;
}
/**
*
* @see org.apache.wicket.MarkupContainer#isTransparentResolver()
*/
@Override
public boolean isTransparentResolver()
{
return true;
}
/**
* @see org.apache.wicket.Component#onComponentTag(org.apache.wicket.markup.ComponentTag)
*/
@Override
protected void onComponentTag(final ComponentTag tag)
{
// Convert <wicket:message /> into <wicket:message>...</wicket:message>
if (tag.isOpenClose())
{
tag.setType(XmlTag.OPEN);
}
super.onComponentTag(tag);
}
}
}