001// *************************************************************************************************************************** 002// * Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE file * 003// * distributed with this work for additional information regarding copyright ownership. The ASF licenses this file * 004// * to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance * 005// * with the License. You may obtain a copy of the License at * 006// * * 007// * http://www.apache.org/licenses/LICENSE-2.0 * 008// * * 009// * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an * 010// * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the * 011// * specific language governing permissions and limitations under the License. * 012// *************************************************************************************************************************** 013package org.apache.juneau.cp; 014 015import static org.apache.juneau.internal.ResourceBundleUtils.*; 016import static org.apache.juneau.internal.StringUtils.*; 017 018import java.text.*; 019import java.util.*; 020import java.util.concurrent.*; 021 022import org.apache.juneau.collections.*; 023import org.apache.juneau.marshall.*; 024 025/** 026 * An enhanced {@link ResourceBundle}. 027 * 028 * <p> 029 * Wraps a ResourceBundle to provide some useful additional functionality. 030 * 031 * <ul> 032 * <li> 033 * Instead of throwing {@link MissingResourceException}, the {@link ResourceBundle#getString(String)} method 034 * will return <js>"{!key}"</js> if the message could not be found. 035 * <li> 036 * Supported hierarchical lookup of resources from parent parent classes. 037 * <li> 038 * Support for easy retrieval of localized bundles. 039 * <li> 040 * Support for generalized resource bundles (e.g. properties files containing keys for several classes). 041 * </ul> 042 * 043 * <p> 044 * The following example shows the basic usage of this class for retrieving localized messages: 045 * 046 * <p class='bcode w800'> 047 * <cc># Contents of MyClass.properties</cc> 048 * <ck>foo</ck> = <cv>foo {0}</cv> 049 * <ck>MyClass.bar</ck> = <cv>bar {0}</cv> 050 * </p> 051 * <p class='bcode w800'> 052 * <jk>public class</jk> MyClass { 053 * <jk>private static final</jk> Messages <jsf>MESSAGES</jsf> = Messages.<jsm>of</jsm>(MyClass.<jk>class</jk>); 054 * 055 * <jk>public void</jk> doFoo() { 056 * 057 * <jc>// A normal property.</jc> 058 * String <jv>foo</jv> = <jsf>MESSAGES</jsf>.getString(<js>"foo"</js>,<js>"x"</js>); <jc>// == "foo x"</jc> 059 * 060 * <jc>// A property prefixed by class name.</jc> 061 * String <jv>bar</jv> = <jsf>MESSAGES</jsf>.getString(<js>"bar"</js>,<js>"x"</js>); <jc>// == "bar x"</jc> 062 * 063 * <jc>// A non-existent property.</jc> 064 * String <jv>baz</jv> = <jsf>MESSAGES</jsf>.getString(<js>"baz"</js>,<js>"x"</js>); <jc>// == "{!baz}"</jc> 065 * } 066 * } 067 * </p> 068 * 069 * <p> 070 * The ability to resolve keys prefixed by class name allows you to place all your messages in a single file such 071 * as a common <js>"Messages.properties"</js> file along with those for other classes. 072 * <p> 073 * The following shows how to retrieve messages from a common bundle: 074 * 075 * <p class='bcode w800'> 076 * <jk>public class</jk> MyClass { 077 * <jk>private static final</jk> Messages <jsf>MESSAGES</jsf> = Messages.<jsm>of</jsm>(MyClass.<jk>class</jk>, <js>"Messages"</js>); 078 * } 079 * </p> 080 * 081 * <p> 082 * Resource bundles are searched using the following base name patterns: 083 * <ul> 084 * <li><js>"{package}.{name}"</js> 085 * <li><js>"{package}.i18n.{name}"</js> 086 * <li><js>"{package}.nls.{name}"</js> 087 * <li><js>"{package}.messages.{name}"</js> 088 * </ul> 089 * 090 * <p> 091 * These patterns can be customized using the {@link MessagesBuilder#baseNames(String...)} method. 092 * 093 * <p> 094 * Localized messages can be retrieved in the following way: 095 * 096* <p class='bcode w800'> 097* <jc>// Return value from Japan locale bundle.</jc> 098* String <jv>foo</jv> = <jsf>MESSAGES</jsf>.forLocale(Locale.<jsf>JAPAN</jsf>).getString(<js>"foo"</js>); 099 * </p> 100 */ 101public class Messages extends ResourceBundle { 102 103 private ResourceBundle rb; 104 private Class<?> c; 105 private Messages parent; 106 private Locale locale; 107 108 // Cache of message bundles per locale. 109 private final ConcurrentHashMap<Locale,Messages> localizedMessages = new ConcurrentHashMap<>(); 110 111 // Cache of virtual keys to actual keys. 112 private final Map<String,String> keyMap; 113 114 private final Set<String> rbKeys; 115 116 /** 117 * Creator. 118 * 119 * @param forClass 120 * The class we're creating this object for. 121 * @return A new builder. 122 */ 123 public static final MessagesBuilder create(Class<?> forClass) { 124 return new MessagesBuilder(forClass); 125 } 126 127 /** 128 * Constructor. 129 * 130 * @param forClass 131 * The class we're creating this object for. 132 * @return A new message bundle belonging to the class. 133 */ 134 public static final Messages of(Class<?> forClass) { 135 return create(forClass).build(); 136 } 137 138 /** 139 * Constructor. 140 * 141 * @param forClass 142 * The class we're creating this object for. 143 * @param name 144 * The bundle name (e.g. <js>"Messages"</js>). 145 * <br>If <jk>null</jk>, uses the class name. 146 * @return A new message bundle belonging to the class. 147 */ 148 public static final Messages of(Class<?> forClass, String name) { 149 return create(forClass).name(name).build(); 150 } 151 152 153 /** 154 * Constructor. 155 * 156 * @param forClass 157 * The class we're creating this object for. 158 * @param rb 159 * The resource bundle we're encapsulating. Can be <jk>null</jk>. 160 * @param locale The locale of these messages. 161 * @param parent 162 * The parent resource. Can be <jk>null</jk>. 163 */ 164 public Messages(Class<?> forClass, ResourceBundle rb, Locale locale, Messages parent) { 165 this.c = forClass; 166 this.rb = rb; 167 this.parent = parent; 168 if (parent != null) 169 setParent(parent); 170 this.locale = locale == null ? Locale.getDefault() : locale; 171 172 Map<String,String> keyMap = new TreeMap<>(); 173 174 String cn = c.getSimpleName() + '.'; 175 if (rb != null) { 176 for (String key : rb.keySet()) { 177 keyMap.put(key, key); 178 if (key.startsWith(cn)) { 179 String shortKey = key.substring(cn.length()); 180 keyMap.put(shortKey, key); 181 } 182 } 183 } 184 if (parent != null) { 185 for (String key : parent.keySet()) { 186 keyMap.put(key, key); 187 if (key.startsWith(cn)) { 188 String shortKey = key.substring(cn.length()); 189 keyMap.put(shortKey, key); 190 } 191 } 192 } 193 194 this.keyMap = Collections.unmodifiableMap(new LinkedHashMap<>(keyMap)); 195 this.rbKeys = rb == null ? Collections.emptySet() : rb.keySet(); 196 } 197 198 /** 199 * Returns this message bundle for the specified locale. 200 * 201 * @param locale The locale to get the messages for. 202 * @return A new {@link Messages} object. Never <jk>null</jk>. 203 */ 204 public Messages forLocale(Locale locale) { 205 if (locale == null) 206 locale = Locale.getDefault(); 207 if (this.locale.equals(locale)) 208 return this; 209 Messages mb = localizedMessages.get(locale); 210 if (mb == null) { 211 Messages parent = this.parent == null ? null : this.parent.forLocale(locale); 212 ResourceBundle rb = this.rb == null ? null : findBundle(this.rb.getBaseBundleName(), locale, c.getClassLoader()); 213 mb = new Messages(c, rb, locale, parent); 214 localizedMessages.put(locale, mb); 215 } 216 return mb; 217 } 218 219 /** 220 * Returns all keys in this resource bundle with the specified prefix. 221 * 222 * <p> 223 * Keys are returned in alphabetical order. 224 * 225 * @param prefix The prefix. 226 * @return The set of all keys in the resource bundle with the prefix. 227 */ 228 public Set<String> keySet(String prefix) { 229 Set<String> set = new LinkedHashSet<>(); 230 for (String s : keySet()) { 231 if (s.equals(prefix) || (s.startsWith(prefix) && s.charAt(prefix.length()) == '.')) 232 set.add(s); 233 } 234 return set; 235 } 236 237 /** 238 * Similar to {@link ResourceBundle#getString(String)} except allows you to pass in {@link MessageFormat} objects. 239 * 240 * @param key The resource bundle key. 241 * @param args Optional {@link MessageFormat}-style arguments. 242 * @return 243 * The resolved value. Never <jk>null</jk>. 244 * <js>"{!key}"</js> if the key is missing. 245 */ 246 public String getString(String key, Object...args) { 247 String s = getString(key); 248 if (s.startsWith("{!")) 249 return s; 250 return format(s, args); 251 } 252 253 /** 254 * Looks for all the specified keys in the resource bundle and returns the first value that exists. 255 * 256 * @param keys The list of possible keys. 257 * @return The resolved value, or <jk>null</jk> if no value is found or the resource bundle is missing. 258 */ 259 public String findFirstString(String...keys) { 260 for (String k : keys) { 261 if (containsKey(k)) 262 return getString(k); 263 } 264 return null; 265 } 266 267 @Override /* ResourceBundle */ 268 protected Object handleGetObject(String key) { 269 String k = keyMap.get(key); 270 if (k == null) 271 return "{!" + key + "}"; 272 try { 273 if (rbKeys.contains(k)) 274 return rb.getObject(k); 275 } catch (MissingResourceException e) { /* Shouldn't happen */ } 276 return parent.handleGetObject(key); 277 } 278 279 @Override /* ResourceBundle */ 280 public boolean containsKey(String key) { 281 return keyMap.containsKey(key); 282 } 283 284 @Override /* ResourceBundle */ 285 public Set<String> keySet() { 286 return keyMap.keySet(); 287 } 288 289 @Override /* ResourceBundle */ 290 public Enumeration<String> getKeys() { 291 return Collections.enumeration(keySet()); 292 } 293 294 @Override 295 public String toString() { 296 OMap om = new OMap(); 297 for (String k : new TreeSet<>(keySet())) 298 om.put(k, getString(k)); 299 return SimpleJson.DEFAULT.toString(om); 300 } 301}