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 * <wicket:message key="myKey"> 052 * This text will be replaced with text from the properties file. 053 * <span wicket:id="amount">[amount]</span>. 054 * <a wicket:id="link"> 055 * <wicket:message key="linkText"/> 056 * </a> 057 * </wicket:message> 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("amount", new Model<String>("$5.00"))); 073 * add(new BookmarkablePageLink<Void>("link", DetailsPage.class)); 074 * </pre> 075 * 076 * This will output 077 * 078 * <pre> 079 * Your balance is $5.00. Click <a href="#">here</a> 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}