001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 *      https://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017
018package org.apache.commons.codec.language.bm;
019
020import java.util.Collections;
021import java.util.EnumMap;
022import java.util.HashSet;
023import java.util.Map;
024import java.util.NoSuchElementException;
025import java.util.Scanner;
026import java.util.Set;
027import java.util.stream.Collectors;
028
029import org.apache.commons.codec.Resources;
030
031/**
032 * Language codes.
033 * <p>
034 * Language codes are typically loaded from resource files. These are UTF-8
035 * encoded text files. They are systematically named following the pattern:
036 * </p>
037 * <blockquote>org/apache/commons/codec/language/bm/${{@link NameType#getName()}
038 * languages.txt</blockquote>
039 * <p>
040 * The format of these resources is the following:
041 * </p>
042 * <ul>
043 * <li><strong>Language:</strong> a single string containing no whitespace</li>
044 * <li><strong>End-of-line comments:</strong> Any occurrence of '//' will cause all text
045 * following on that line to be discarded as a comment.</li>
046 * <li><strong>Multi-line comments:</strong> Any line starting with '/*' will start
047 * multi-line commenting mode. This will skip all content until a line ending in
048 * '*' and '/' is found.</li>
049 * <li><strong>Blank lines:</strong> All blank lines will be skipped.</li>
050 * </ul>
051 * <p>
052 * Ported from language.php
053 * </p>
054 * <p>
055 * This class is immutable and thread-safe.
056 * </p>
057 *
058 * @since 1.6
059 */
060public class Languages {
061    // Implementation note: This class is divided into two sections. The first part
062    // is a static factory interface that
063    // exposes org/apache/commons/codec/language/bm/%s_languages.txt for %s in
064    // NameType.* as a list of supported
065    // languages, and a second part that provides instance methods for accessing
066    // this set for supported languages.
067
068    /**
069     * A set of languages.
070     */
071    public abstract static class LanguageSet {
072
073        /**
074         * Gets a language set for the given languages.
075         *
076         * @param languages A language set.
077         * @return A LanguageSet.
078         */
079        public static LanguageSet from(final Set<String> languages) {
080            return languages.isEmpty() ? NO_LANGUAGES : new SomeLanguages(languages);
081        }
082
083        /**
084         * Constructs a new instance for subclasses.
085         */
086        public LanguageSet() {
087            // empty
088        }
089
090        /**
091         * Tests whether this instance contains the given value.
092         *
093         * @param language The value to test.
094         * @return whether this instance contains the given value.
095         */
096        public abstract boolean contains(String language);
097
098        /**
099         * 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}