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 }