View Javadoc
1   /*
2    * This file is part of Indicators.
3    *
4    * Indicators is free software: you can redistribute it and/or modify
5    * it under the terms of the GNU General Public License as published by
6    * the Free Software Foundation, either version 3 of the License, or
7    * (at your option) any later version.
8    *
9    * Indicators is distributed in the hope that it will be useful,
10   * but WITHOUT ANY WARRANTY; without even the implied warranty of
11   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12   * GNU General Public License for more details.
13   *
14   * You should have received a copy of the GNU General Public License
15   * along with Indicators. If not, see <https://www.gnu.org/licenses/>.
16   */
17  package fr.inrae.agroclim.indicators.resources;
18  
19  import static java.lang.Math.abs;
20  
21  import java.util.List;
22  import java.util.Locale;
23  import java.util.Objects;
24  import java.util.Optional;
25  import java.util.ResourceBundle;
26  import java.util.function.BiFunction;
27  import java.util.function.BiPredicate;
28  import java.util.function.Predicate;
29  
30  /**
31   * Localized messages with plural handling à la GWT.
32   *
33   * Last change $Date$
34   *
35   * @author $Author$
36   * @version $Revision$
37   */
38  public final class I18n {
39      /**
40       * Accepted operators, ordered.
41       */
42      public enum Operator implements BiFunction<Integer, Integer, Boolean> {
43          /**
44           * Equals.
45           */
46          AEQ("=", Objects::equals),
47          /**
48           * Inferior or equals.
49           */
50          BINFEQ("<=", (a, b) -> a <= b),
51          /**
52           * Strictly inferior.
53           */
54          CINF("<", (a, b) -> a < b),
55          /**
56           * Superior or equals.
57           */
58          DSUPEQ(">=", (a, b) -> a >= b),
59          /**
60           * Strictly superior.
61           */
62          ESUP(">", (a, b) -> a > b);
63          /**
64           * Guess the right operator in the string comparison.
65           *
66           * @param string string comparison (eg.: ">=10")
67           * @return operator matching the string comparison
68           */
69          static Optional<Operator> extract(final String string) {
70              for (final Operator op : values()) {
71                  if (string.startsWith(op.symbol)) {
72                      return Optional.of(op);
73                  }
74              }
75              return Optional.empty();
76          }
77          /**
78           * Comparison function of the operator.
79           */
80          private final BiPredicate<Integer, Integer> function;
81          /**
82           * String representation of the operator.
83           */
84          private final String symbol;
85  
86          /**
87           * Constructor.
88           *
89           * @param string String representation of the operator.
90           * @param func Comparison function of the operator.
91           */
92          Operator(final String string,
93                  final BiPredicate<Integer, Integer> func) {
94              symbol = string;
95              function = func;
96          }
97  
98          @Override
99          public Boolean apply(final Integer arg0, final Integer arg1) {
100             return function.test(arg0, arg1);
101         }
102 
103         /**
104          * @return length of string representation
105          */
106         public int getLength() {
107             return symbol.length();
108         }
109     }
110 
111     /**
112      * Standard suffix for keys in .properties, à la GWT.
113      *
114      * http://www.gwtproject.org/doc/latest/DevGuideI18nPluralForms.html
115      */
116     @SuppressWarnings("checkstyle:MagicNumber")
117     public enum PluralSuffix implements Predicate<Integer> {
118         /**
119          * The count is 0.
120          */
121         NONE(nb -> nb == 0),
122         /**
123          * The count is 1.
124          */
125         ONE(nb -> nb == 1),
126         /**
127          * The count is 2.
128          */
129         TWO(nb -> nb == 2),
130         /**
131          * The last two digits are from 03-10.
132          */
133         FEW(nb -> nb % 100 >= 3 && nb % 100 <= 10),
134         /**
135          * the last two digits are from 11-99.
136          */
137         MANY(nb -> nb % 100 >= 11 && nb % 100 <= 99);
138 
139         /**
140          * Find the suffix according to the value.
141          *
142          * @param value value to find the prefix
143          * @return found prefix or empty
144          */
145         static Optional<String> getSuffix(final Integer value) {
146             for (final PluralSuffix suffix : values()) {
147                 if (suffix.test(value)) {
148                     return Optional.of(suffix.name().toLowerCase());
149                 }
150             }
151             return Optional.empty();
152         }
153 
154         /**
155          * Predicate to check if the value match the plural suffix.
156          */
157         private final Predicate<Integer> predicate;
158 
159         /**
160          * Constructor.
161          *
162          * @param pr predicate for the prefix
163          */
164         PluralSuffix(final Predicate<Integer> pr) {
165             predicate = pr;
166         }
167 
168         @Override
169         public boolean test(final Integer value) {
170             if (value != null) {
171                 return predicate.test(abs(value));
172             }
173             return false;
174         }
175 
176     }
177 
178     /**
179      * Check if the comparison string matches the value.
180      *
181      * @param comparison comparison string (eg.: ">10")
182      * @param plural value
183      * @return if the comparison string matches the value.
184      */
185     static boolean matches(final String comparison, final int plural) {
186         final Optional<Operator> operator = Operator.extract(comparison);
187         if (operator.isPresent()) {
188             final Operator op = operator.get();
189             String val = comparison.substring(op.getLength());
190             if (val != null) {
191                 val = val.trim();
192                 if (!val.isEmpty()) {
193                     final Integer value = Integer.valueOf(val);
194                     return op.apply(plural, value);
195                 }
196             }
197         }
198         return false;
199     }
200 
201     /**
202      * Resources from .properties file.
203      */
204     private final Resources resources;
205 
206     /**
207      * Constructor.
208      *
209      * @param bundle Resources bundle build outside.
210      */
211     public I18n(final ResourceBundle bundle) {
212         resources = new Resources(bundle);
213     }
214 
215     /**
216      * Constructor.
217      *
218      * @param bundleName Path of .property resource.
219      * @param locale The locale for the bundle.
220      */
221     public I18n(final String bundleName, final Locale locale) {
222         resources = new Resources(bundleName, locale);
223     }
224 
225     /**
226      * Return message with inlined arguments.
227      *
228      * @param plural value for plural form
229      * @param key message key
230      * @param messageArguments arguments for the message.
231      * @return message with arguments or exclamation message
232      */
233     public String format(final int plural, final String key,
234             final Object... messageArguments) {
235         String keyWithSuffix;
236 
237         // the suffix for the value
238         keyWithSuffix = key + "[=" + plural + "]";
239         if (resources.getKeys().contains(keyWithSuffix)) {
240             return resources.format(keyWithSuffix, messageArguments);
241         }
242 
243         // find the right standard suffix
244         final Optional<String> suffix = PluralSuffix.getSuffix(plural);
245         if (suffix.isPresent()) {
246             keyWithSuffix = key + "[" + suffix.get() + "]";
247             if (resources.getKeys().contains(keyWithSuffix)) {
248                 return resources.format(keyWithSuffix, messageArguments);
249             }
250         }
251 
252         // with comparators <, <=, >, >=
253         final List<String> suffixes = resources.getKeys().stream() //
254                 .filter(k -> k.startsWith(key + "[") && k.endsWith("]")) //
255                 .map(k -> k.substring(key.length() + 1, k.length() - 1)) //
256                 .toList();
257         for (final String suf: suffixes) {
258             if (matches(suf, plural)) {
259                 keyWithSuffix = key + "[" + suf + "]";
260                 return resources.format(keyWithSuffix, messageArguments);
261             }
262         }
263         // if not defined, used default
264         return resources.format(key, messageArguments);
265     }
266 
267     /**
268      * Return message with inlined arguments.
269      *
270      * @param key message key
271      * @param messageArguments arguments for the message.
272      * @return message with arguments or exclamation message
273      */
274     public String format(final String key,
275             final Object... messageArguments) {
276         return resources.format(key, messageArguments);
277     }
278 
279     /**
280      * Retrieve message from key.
281      *
282      * @param key message key
283      * @return message value or exclamation message
284      */
285     public String get(final String key) {
286         return resources.getString(key);
287     }
288 
289     /**
290      * @return Path of .property resource.
291      */
292     public String getBundleName() {
293         return resources.getBundleName();
294     }
295 
296     /**
297      * @param locale Locale for the bundle.
298      */
299     public void setLocale(final Locale locale) {
300         resources.setLocale(locale);
301     }
302 }