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 final DecimalFormatSymbols symbols = DecimalFormatSymbols.getInstance(getCurrentLocale());
210 if (symbols.getGroupingSeparator() == '\u202f') {
211 // change le séparateur de milliers en France par le séparateur de milliers d'avant Java 13,
212 // pour les rapports PDF qui ne comprennent pas \u202f (en iText 2.1.7 ou openpdf 1.3.30)
213 symbols.setGroupingSeparator('\u00a0');
214 }
215 return symbols;
216 }
217
218 public static DateFormat createDateFormat() {
219 // attention ces instances de DateFormat ne doivent pas être statiques
220 // car DateFormat n'est pas multi-thread-safe,
221 // voir http://java.sun.com/javase/6/docs/api/java/text/DateFormat.html#synchronization
222 return DateFormat.getDateInstance(DateFormat.SHORT, getCurrentLocale());
223 }
224
225 public static DateFormat createDateAndTimeFormat() {
226 return DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.SHORT,
227 getCurrentLocale());
228 }
229
230 public static DateFormat createDurationFormat() {
231 // Locale.FRENCH et non getCurrentLocale() car pour une durée on veut
232 // "00:01:02" (1min 02s) et non "12:01:02 AM"
233 final DateFormat durationFormat = DateFormat.getTimeInstance(DateFormat.MEDIUM,
234 Locale.FRENCH);
235 // une durée ne dépend pas de l'horaire été/hiver du fuseau horaire de Paris
236 durationFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
237 return durationFormat;
238 }
239
240 public static String getCurrentDate() {
241 return createDateFormat().format(new Date());
242 }
243
244 public static String getCurrentDateAndTime() {
245 return createDateAndTimeFormat().format(new Date());
246 }
247
248 private static Locale getFixedLocale() {
249 final String locale = Parameter.LOCALE.getValue();
250 if (locale != null) {
251 for (final Locale l : Locale.getAvailableLocales()) {
252 if (l.toString().equals(locale)) {
253 return l;
254 }
255 }
256 }
257 return null;
258 }
259 }
260