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