View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    *
9    *      https://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  
18  package org.apache.commons.codec.language.bm;
19  
20  import java.util.Collections;
21  import java.util.EnumMap;
22  import java.util.HashSet;
23  import java.util.Map;
24  import java.util.NoSuchElementException;
25  import java.util.Scanner;
26  import java.util.Set;
27  import java.util.stream.Collectors;
28  
29  import org.apache.commons.codec.Resources;
30  
31  /**
32   * Language codes.
33   * <p>
34   * Language codes are typically loaded from resource files. These are UTF-8
35   * encoded text files. They are systematically named following the pattern:
36   * </p>
37   * <blockquote>org/apache/commons/codec/language/bm/${{@link NameType#getName()}
38   * languages.txt</blockquote>
39   * <p>
40   * The format of these resources is the following:
41   * </p>
42   * <ul>
43   * <li><strong>Language:</strong> a single string containing no whitespace</li>
44   * <li><strong>End-of-line comments:</strong> Any occurrence of '//' will cause all text
45   * following on that line to be discarded as a comment.</li>
46   * <li><strong>Multi-line comments:</strong> Any line starting with '/*' will start
47   * multi-line commenting mode. This will skip all content until a line ending in
48   * '*' and '/' is found.</li>
49   * <li><strong>Blank lines:</strong> All blank lines will be skipped.</li>
50   * </ul>
51   * <p>
52   * Ported from language.php
53   * </p>
54   * <p>
55   * This class is immutable and thread-safe.
56   * </p>
57   *
58   * @since 1.6
59   */
60  public class Languages {
61      // Implementation note: This class is divided into two sections. The first part
62      // is a static factory interface that
63      // exposes org/apache/commons/codec/language/bm/%s_languages.txt for %s in
64      // NameType.* as a list of supported
65      // languages, and a second part that provides instance methods for accessing
66      // this set for supported languages.
67  
68      /**
69       * A set of languages.
70       */
71      public abstract static class LanguageSet {
72  
73          /**
74           * Gets a language set for the given languages.
75           *
76           * @param languages A language set.
77           * @return A LanguageSet.
78           */
79          public static LanguageSet from(final Set<String> languages) {
80              return languages.isEmpty() ? NO_LANGUAGES : new SomeLanguages(languages);
81          }
82  
83          /**
84           * Constructs a new instance for subclasses.
85           */
86          public LanguageSet() {
87              // empty
88          }
89  
90          /**
91           * Tests whether this instance contains the given value.
92           *
93           * @param language The value to test.
94           * @return whether this instance contains the given value.
95           */
96          public abstract boolean contains(String language);
97  
98          /**
99           * Gets any of this instance's element.
100          *
101          * @return any of this instance's element.
102          */
103         public abstract String getAny();
104 
105         /**
106          * Tests whether this instance is empty.
107          *
108          * @return whether this instance is empty.
109          */
110         public abstract boolean isEmpty();
111 
112         /**
113          * Tests whether this instance contains a single element.
114          *
115          * @return whether this instance contains a single element.
116          */
117         public abstract boolean isSingleton();
118 
119         abstract LanguageSet merge(LanguageSet other);
120 
121         /**
122          * Returns an instance restricted to this instances and the given values'.
123          *
124          * @param other The other instance.
125          * @return An instance restricted to this instances and the given values'.
126          */
127         public abstract LanguageSet restrictTo(LanguageSet other);
128     }
129 
130     /**
131      * Some languages, explicitly enumerated.
132      */
133     public static final class SomeLanguages extends LanguageSet {
134         private final Set<String> languages;
135 
136         private SomeLanguages(final Set<String> languages) {
137             this.languages = Collections.unmodifiableSet(languages);
138         }
139 
140         @Override
141         public boolean contains(final String language) {
142             return this.languages.contains(language);
143         }
144 
145         @Override
146         public String getAny() {
147             return this.languages.iterator().next();
148         }
149 
150         /**
151          * Gets the language strings
152          *
153          * @return The languages strings.
154          */
155         public Set<String> getLanguages() {
156             return this.languages;
157         }
158 
159         @Override
160         public boolean isEmpty() {
161             return this.languages.isEmpty();
162         }
163 
164         @Override
165         public boolean isSingleton() {
166             return this.languages.size() == 1;
167         }
168 
169         @Override
170         public LanguageSet merge(final LanguageSet other) {
171             if (other == NO_LANGUAGES) {
172                 return this;
173             }
174             if (other == ANY_LANGUAGE) {
175                 return other;
176             }
177             final SomeLanguages someLanguages = (SomeLanguages) other;
178             final Set<String> set = new HashSet<>(languages);
179             set.addAll(someLanguages.languages);
180             return from(set);
181         }
182 
183         @Override
184         public LanguageSet restrictTo(final LanguageSet other) {
185             if (other == NO_LANGUAGES) {
186                 return other;
187             }
188             if (other == ANY_LANGUAGE) {
189                 return this;
190             }
191             final SomeLanguages someLanguages = (SomeLanguages) other;
192             return from(languages.stream().filter(lang -> someLanguages.languages.contains(lang)).collect(Collectors.toSet()));
193         }
194 
195         @Override
196         public String toString() {
197             return "Languages(" + languages.toString() + ")";
198         }
199 
200     }
201 
202     /**
203      * Marker for any language.
204      */
205     public static final String ANY = "any";
206 
207     private static final Map<NameType, Languages> LANGUAGES = new EnumMap<>(NameType.class);
208 
209     /**
210      * No languages at all.
211      */
212     public static final LanguageSet NO_LANGUAGES = new LanguageSet() {
213 
214         @Override
215         public boolean contains(final String language) {
216             return false;
217         }
218 
219         /**
220          * Always throws {@link NoSuchElementException} because this language set does not identify a specific language.
221          *
222          * @return Never returns normally.
223          * @throws NoSuchElementException Thrown whenever this method is invoked.
224          */
225         @Override
226         public String getAny() {
227             throw new NoSuchElementException("Can't fetch any language from the empty language set.");
228         }
229 
230         @Override
231         public boolean isEmpty() {
232             return true;
233         }
234 
235         @Override
236         public boolean isSingleton() {
237             return false;
238         }
239 
240         @Override
241         public LanguageSet merge(final LanguageSet other) {
242             return other;
243         }
244 
245         @Override
246         public LanguageSet restrictTo(final LanguageSet other) {
247             return this;
248         }
249 
250         @Override
251         public String toString() {
252             return "NO_LANGUAGES";
253         }
254     };
255 
256     /**
257      * Any/all languages.
258      */
259     public static final LanguageSet ANY_LANGUAGE = new LanguageSet() {
260 
261         @Override
262         public boolean contains(final String language) {
263             return true;
264         }
265 
266         /**
267          * Always throws {@link NoSuchElementException} because this language set does not identify a specific language.
268          *
269          * @return Never returns normally.
270          * @throws NoSuchElementException Thrown whenever this method is invoked.
271          */
272         @Override
273         public String getAny() {
274             throw new NoSuchElementException("Can't fetch any language from the any language set.");
275         }
276 
277         @Override
278         public boolean isEmpty() {
279             return false;
280         }
281 
282         @Override
283         public boolean isSingleton() {
284             return false;
285         }
286 
287         @Override
288         public LanguageSet merge(final LanguageSet other) {
289             return other;
290         }
291 
292         @Override
293         public LanguageSet restrictTo(final LanguageSet other) {
294             return other;
295         }
296 
297         @Override
298         public String toString() {
299             return "ANY_LANGUAGE";
300         }
301     };
302 
303     static {
304         for (final NameType s : NameType.values()) {
305             LANGUAGES.put(s, getInstance(langResourceName(s)));
306         }
307     }
308 
309     /**
310      * Gets an instance for the given name type.
311      *
312      * @param nameType The name type to lookup.
313      * @return An instance for the given name type.
314      */
315     public static Languages getInstance(final NameType nameType) {
316         return LANGUAGES.get(nameType);
317     }
318 
319     /**
320      * Gets a new instance for the given resource name.
321      *
322      * @param languagesResourceName The resource name to lookup.
323      * @return A new instance.
324      */
325     public static Languages getInstance(final String languagesResourceName) {
326         // read languages list
327         final Set<String> ls = new HashSet<>();
328         try (Scanner lsScanner = new Scanner(Resources.getInputStream(languagesResourceName),
329                 ResourceConstants.ENCODING)) {
330             boolean inExtendedComment = false;
331             while (lsScanner.hasNextLine()) {
332                 final String line = lsScanner.nextLine().trim();
333                 if (inExtendedComment) {
334                     if (line.endsWith(ResourceConstants.EXT_CMT_END)) {
335                         inExtendedComment = false;
336                     }
337                 } else if (line.startsWith(ResourceConstants.EXT_CMT_START)) {
338                     inExtendedComment = true;
339                 } else if (!line.isEmpty()) {
340                     ls.add(line);
341                 }
342             }
343             return new Languages(Collections.unmodifiableSet(ls));
344         }
345     }
346 
347     private static String langResourceName(final NameType nameType) {
348         return String.format("/org/apache/commons/codec/language/bm/%s_languages.txt", nameType.getName());
349     }
350 
351     private final Set<String> languages;
352 
353     private Languages(final Set<String> languages) {
354         this.languages = languages;
355     }
356 
357     /**
358      * Gets the language set.
359      *
360      * @return The language set.
361      */
362     public Set<String> getLanguages() {
363         return this.languages;
364     }
365 }