/* * 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.Map; import org.apache.wicket.Application; import org.apache.wicket.Component; import org.apache.wicket.MarkupContainer; import org.apache.wicket.WicketRuntimeException; import org.apache.wicket.core.util.lang.PropertyResolver; 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.TagType; import org.apache.wicket.model.Model; import org.apache.wicket.request.Response; import org.apache.wicket.response.StringResponse; import org.apache.wicket.util.string.Strings; import org.apache.wicket.util.string.interpolator.MapVariableInterpolator; import org.apache.wicket.util.value.IValueMap; 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. * <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<Void>("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. * * @see org.apache.wicket.settings.ResourceSettings#setThrowExceptionOnMissingResource(boolean) * @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); /** */ public static final String MESSAGE = "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"; /** * The name of the attribute that defines the resource key */ public static final String KEY_ATTRIBUTE = "key"; /** * The name of the attribute that defines whether the resource value should be HTML escaped */ public static final String ESCAPE_ATTRIBUTE = "escape"; @Override public Component resolve(final MarkupContainer container, final MarkupStream markupStream, final ComponentTag tag) { if (tag instanceof WicketTag) { WicketTag wtag = (WicketTag)tag; if (wtag.isMessageTag()) { IValueMap attributes = wtag.getAttributes(); String messageKey = attributes.getString(KEY_ATTRIBUTE); if (Strings.isEmpty(messageKey)) { throw new MarkupException( "Wrong format of <wicket:message key='xxx'>: attribute 'key' is missing"); } boolean escape = attributes.getBoolean(ESCAPE_ATTRIBUTE); final String id = wtag.getId(); MessageContainer label = new MessageContainer(id, messageKey, escape); label.setRenderBodyOnly(container.getApplication() .getMarkupSettings() .getStripWicketTags()); return label; } } // We were not able to handle the tag return null; } /** * 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 implements IComponentResolver { private static final long serialVersionUID = 1L; private static final String NOT_FOUND = "[Warning: Property for '%s' not found]"; private final boolean escapeValue; /** * Construct. * * @param id * @param messageKey * @param escapeValue */ private MessageContainer(final String id, final String messageKey, boolean escapeValue) { // The message key becomes the model super(id, new Model<>(messageKey)); this.escapeValue = escapeValue; setEscapeModelStrings(false); } @Override public Component resolve(MarkupContainer container, MarkupStream markupStream, ComponentTag tag) { return getParent().get(tag.getId()); } @Override protected void onComponentTag(final ComponentTag tag) { // Convert <wicket:message /> into <wicket:message>...</wicket:message> if (tag.isOpenClose()) { tag.setType(TagType.OPEN); } super.onComponentTag(tag); } @Override public 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 { 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); // If open tag was open-close if (markupStream.isCurrentIndexInsideTheStream() == false) { String formatedNotFound = String.format(NOT_FOUND, key); getResponse().write(formatedNotFound); } super.onComponentTagBody(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 CharSequence text = new MapVariableInterpolator(value, childTags) { @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(); if (escapeValue) { text = Strings.escapeMarkup(text); } getResponse().write(text); // Make sure all of the children were rendered for (String id : childTags.keySet()) { 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 ComponentTag tag = markupStream.getPreviousTag(); // 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.isCurrentIndexInsideTheStream() && !markupStream.get().closes(openTag)) { MarkupElement element = markupStream.get(); // If it a tag like <wicket..> or <span wicket:id="..." > if ((element instanceof ComponentTag) && !markupStream.atCloseTag()) { ComponentTag currentTag = (ComponentTag)element; String id = currentTag.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 = ComponentResolvers.resolve(getParent(), markupStream, currentTag, null); // Must not be a Page and it must be connected to a parent. if (component.getParent() == null) { component = null; } } if (component != null) { component.render(); markupStream.skipComponent(); } else { markupStream.next(); } childTags.put(id, response.getBuffer()); } finally { // Restore the original response getRequestCycle().setResponse(webResponse); } } else { markupStream.next(); } } } return childTags; } } }