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("&""&amp;").replace("<""&lt;").replace(">""&gt;")
138                 .replace("'""&apos;").replace("\"", "&quot;");
139         if (encodeSpace) {
140             result = result.replace(" ""&nbsp;");
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