1 /*
2 * Copyright 2008-2019 by Emeric Vernat
3 *
4 * This file is part of Java Melody.
5 *
6 * Licensed under the Apache License, Version 2.0 (the "License");
7 * you may not use this file except in compliance with the License.
8 * You may obtain a copy of the License at
9 *
10 * http://www.apache.org/licenses/LICENSE-2.0
11 *
12 * Unless required by applicable law or agreed to in writing, software
13 * distributed under the License is distributed on an "AS IS" BASIS,
14 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 * See the License for the specific language governing permissions and
16 * limitations under the License.
17 */
18 package net.bull.javamelody.internal.common;
19
20 import java.io.IOException;
21 import java.io.Writer;
22 import java.text.DateFormat;
23 import java.text.DecimalFormat;
24 import java.text.DecimalFormatSymbols;
25 import java.text.MessageFormat;
26 import java.util.Date;
27 import java.util.Locale;
28 import java.util.ResourceBundle;
29 import java.util.TimeZone;
30
31 import net.bull.javamelody.Parameter;
32
33 /**
34 * Classe de gestion des traductions et de l'internationalisation (formats de dates et de nombre).
35 * La locale pour les rapports vient de la requête et est associée au thread courant.
36 * @author Emeric Vernat
37 */
38 public final class I18N {
39 // RESOURCE_BUNDLE_BASE_NAME vaut "net.bull.javamelody.resource.translations"
40 // ce qui charge net.bull.javamelody.resource.translations.properties
41 // et net.bull.javamelody.resource.translations_fr.properties
42 // (Parameters.getResourcePath("translations") seul ne fonctionne pas si on est dans un jar/war)
43 private static final String RESOURCE_BUNDLE_BASE_NAME = Parameters
44 .getResourcePath("translations").replace('/', '.').substring(1);
45 private static final ThreadLocal<Locale> LOCALE_CONTEXT = new ThreadLocal<>();
46 // Locale.ROOT needs 1.6
47 private static final Locale ROOT_LOCALE = Locale.ROOT;
48
49 private static final Locale FIXED_LOCALE = getFixedLocale();
50
51 private I18N() {
52 super();
53 }
54
55 /**
56 * Définit la locale (langue et formats dates et nombres) pour le thread courant.
57 * @param locale Locale
58 */
59 public static void bindLocale(Locale locale) {
60 LOCALE_CONTEXT.set(locale);
61 }
62
63 /**
64 * Retourne la locale pour le thread courant ou la locale par défaut si elle n'a pas été définie.
65 * @return Locale
66 */
67 public static Locale getCurrentLocale() {
68 if (FIXED_LOCALE != null) {
69 return FIXED_LOCALE;
70 }
71 final Locale currentLocale = LOCALE_CONTEXT.get();
72 if (currentLocale == null) {
73 return Locale.getDefault();
74 }
75 return currentLocale;
76 }
77
78 /**
79 * Retourne les traductions pour la locale courante.
80 * @return Locale
81 */
82 public static ResourceBundle getResourceBundle() {
83 final Locale currentLocale = getCurrentLocale();
84 if (Locale.ENGLISH.getLanguage().equals(currentLocale.getLanguage())) {
85 // there is no translations_en.properties because translations.properties is in English
86 // but if user is English, do not let getBundle fallback on server's default locale
87 return ResourceBundle.getBundle(RESOURCE_BUNDLE_BASE_NAME, ROOT_LOCALE);
88 }
89 // and if user is not English, use the bundle if it exists for his/her Locale
90 // or the bundle for the server's default locale if it exists
91 // or default (English) bundle otherwise
92 return ResourceBundle.getBundle(RESOURCE_BUNDLE_BASE_NAME, currentLocale);
93 }
94
95 /**
96 * Enlève le lien entre la locale et le thread courant.
97 */
98 public static void unbindLocale() {
99 LOCALE_CONTEXT.remove();
100 }
101
102 /**
103 * Retourne une traduction dans la locale courante.
104 * @param key clé d'un libellé dans les fichiers de traduction
105 * @return String
106 */
107 public static String getString(String key) {
108 return getResourceBundle().getString(key);
109 }
110
111 /**
112 * Retourne une traduction dans la locale courante et insère les arguments aux positions {i}.
113 * @param key clé d'un libellé dans les fichiers de traduction
114 * @param arguments Valeur à inclure dans le résultat
115 * @return String
116 */
117 public static String getFormattedString(String key, Object... arguments) {
118 // échappement des quotes qui sont des caractères spéciaux pour MessageFormat
119 final String string = getString(key).replace("'", "''");
120 return new MessageFormat(string, getCurrentLocale()).format(arguments);
121 }
122
123 public static String urlEncode(String text) {
124 return text.replace("\\", "\\\\").replace("\n", "\\n").replace("\"", "%22").replace("'",
125 "%27");
126 }
127
128 /**
129 * Encode pour affichage en html.
130 * @param text message à encoder
131 * @param encodeSpace booléen selon que les espaces sont encodés en nbsp (insécables)
132 * @param encodeNewLine booléen selon que les retours à la ligne sont encodés en br
133 * @return String
134 */
135 public static String htmlEncode(String text, boolean encodeSpace, boolean encodeNewLine) {
136 // ces encodages html sont incomplets mais suffisants pour le monitoring
137 String result = text.replace("&", "&").replace("<", "<").replace(">", ">")
138 .replace("'", "'").replace("\"", """);
139 if (encodeSpace) {
140 result = result.replace(" ", " ");
141 }
142 if (encodeNewLine) {
143 result = result.replace("\n", "<br/>");
144 }
145 return result;
146 }
147
148 /**
149 * Encode pour affichage en html.
150 * @param text message à encoder
151 * @param encodeSpace booléen selon que les espaces sont encodés en nbsp (insécables)
152 * @return String
153 */
154 public static String htmlEncode(String text, boolean encodeSpace) {
155 return htmlEncode(text, encodeSpace, true);
156 }
157
158 /**
159 * Écrit un texte dans un flux en remplaçant dans le texte les clés entourées de deux '#'
160 * par leurs traductions dans la locale courante.
161 * @param html texte html avec éventuellement des #clé#
162 * @param writer flux
163 * @throws IOException e
164 */
165 public static void writeTo(String html, Writer writer) throws IOException {
166 int index = html.indexOf('#');
167 if (index == -1) {
168 writer.write(html);
169 } else {
170 final ResourceBundle resourceBundle = getResourceBundle();
171 int begin = 0;
172 while (index != -1) {
173 writer.write(html, begin, index - begin);
174 final int nextIndex = html.indexOf('#', index + 1);
175 final String key = html.substring(index + 1, nextIndex);
176 writer.write(resourceBundle.getString(key));
177 begin = nextIndex + 1;
178 index = html.indexOf('#', begin);
179 }
180 writer.write(html, begin, html.length() - begin);
181 }
182 }
183
184 /**
185 * Écrit un texte, puis un retour chariot, dans un flux en remplaçant dans le texte les clés entourées de deux '#'
186 * par leurs traductions dans la locale courante.
187 * @param html texte html avec éventuellement des #clé#
188 * @param writer flux
189 * @throws IOException e
190 */
191 public static void writelnTo(String html, Writer writer) throws IOException {
192 writeTo(html, writer);
193 writer.write('\n');
194 }
195
196 // méthodes utilitaires de formatage de dates et de nombres
197 public static DecimalFormat createIntegerFormat() {
198 // attention ces instances de DecimalFormat ne doivent pas être statiques
199 // car DecimalFormat n'est pas multi-thread-safe,
200 return new DecimalFormat("#,##0", getDecimalFormatSymbols());
201 }
202
203 public static DecimalFormat createPercentFormat() {
204 return new DecimalFormat("0.00", getDecimalFormatSymbols());
205 }
206
207 private static DecimalFormatSymbols getDecimalFormatSymbols() {
208 // optimisation mémoire (si Java 1.6)
209 return DecimalFormatSymbols.getInstance(getCurrentLocale());
210 }
211
212 public static DateFormat createDateFormat() {
213 // attention ces instances de DateFormat ne doivent pas être statiques
214 // car DateFormat n'est pas multi-thread-safe,
215 // voir http://java.sun.com/javase/6/docs/api/java/text/DateFormat.html#synchronization
216 return DateFormat.getDateInstance(DateFormat.SHORT, getCurrentLocale());
217 }
218
219 public static DateFormat createDateAndTimeFormat() {
220 return DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.SHORT,
221 getCurrentLocale());
222 }
223
224 public static DateFormat createDurationFormat() {
225 // Locale.FRENCH et non getCurrentLocale() car pour une durée on veut
226 // "00:01:02" (1min 02s) et non "12:01:02 AM"
227 final DateFormat durationFormat = DateFormat.getTimeInstance(DateFormat.MEDIUM,
228 Locale.FRENCH);
229 // une durée ne dépend pas de l'horaire été/hiver du fuseau horaire de Paris
230 durationFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
231 return durationFormat;
232 }
233
234 public static String getCurrentDate() {
235 return createDateFormat().format(new Date());
236 }
237
238 public static String getCurrentDateAndTime() {
239 return createDateAndTimeFormat().format(new Date());
240 }
241
242 private static Locale getFixedLocale() {
243 final String locale = Parameter.LOCALE.getValue();
244 if (locale != null) {
245 for (final Locale l : Locale.getAvailableLocales()) {
246 if (l.toString().equals(locale)) {
247 return l;
248 }
249 }
250 }
251 return null;
252 }
253 }
254