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 *      http://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.io.InputStream;
021import java.util.Collections;
022import java.util.EnumMap;
023import java.util.HashSet;
024import java.util.Map;
025import java.util.NoSuchElementException;
026import java.util.Scanner;
027import java.util.Set;
028
029/**
030 * Language codes.
031 * <p>
032 * Language codes are typically loaded from resource files. These are UTF-8 encoded text files. They are
033 * systematically named following the pattern:
034 * <blockquote>org/apache/commons/codec/language/bm/${{@link NameType#getName()} languages.txt</blockquote>
035 * <p>
036 * The format of these resources is the following:
037 * <ul>
038 * <li><b>Language:</b> a single string containing no whitespace</li>
039 * <li><b>End-of-line comments:</b> Any occurrence of '//' will cause all text following on that line to be
040 * discarded as a comment.</li>
041 * <li><b>Multi-line comments:</b> Any line starting with '/*' will start multi-line commenting mode.
042 * This will skip all content until a line ending in '*' and '/' is found.</li>
043 * <li><b>Blank lines:</b> All blank lines will be skipped.</li>
044 * </ul>
045 * <p>
046 * Ported from language.php
047 * <p>
048 * This class is immutable and thread-safe.
049 *
050 * @since 1.6
051 * @version $Id$
052 */
053public class Languages {
054    // Implementation note: This class is divided into two sections. The first part is a static factory interface that
055    // exposes org/apache/commons/codec/language/bm/%s_languages.txt for %s in NameType.* as a list of supported
056    // languages, and a second part that provides instance methods for accessing this set for supported languages.
057
058    /**
059     * A set of languages.
060     */
061    public static abstract class LanguageSet {
062
063        public static LanguageSet from(final Set<String> langs) {
064            return langs.isEmpty() ? NO_LANGUAGES : new SomeLanguages(langs);
065        }
066
067        public abstract boolean contains(String language);
068
069        public abstract String getAny();
070
071        public abstract boolean isEmpty();
072
073        public abstract boolean isSingleton();
074
075        public abstract LanguageSet restrictTo(LanguageSet other);
076
077        abstract LanguageSet merge(LanguageSet other);
078    }
079
080    /**
081     * Some languages, explicitly enumerated.
082     */
083    public static final class SomeLanguages extends LanguageSet {
084        private final Set<String> languages;
085
086        private SomeLanguages(final Set<String> languages) {
087            this.languages = Collections.unmodifiableSet(languages);
088        }
089
090        @Override
091        public boolean contains(final String language) {
092            return this.languages.contains(language);
093        }
094
095        @Override
096        public String getAny() {
097            return this.languages.iterator().next();
098        }
099
100        public Set<String> getLanguages() {
101            return this.languages;
102        }
103
104        @Override
105        public boolean isEmpty() {
106            return this.languages.isEmpty();
107        }
108
109        @Override
110        public boolean isSingleton() {
111            return this.languages.size() == 1;
112        }
113
114        @Override
115        public LanguageSet restrictTo(final LanguageSet other) {
116            if (other == NO_LANGUAGES) {
117                return other;
118            } else if (other == ANY_LANGUAGE) {
119                return this;
120            } else {
121                final SomeLanguages sl = (SomeLanguages) other;
122                final Set<String> ls = new HashSet<>(Math.min(languages.size(), sl.languages.size()));
123                for (final String lang : languages) {
124                    if (sl.languages.contains(lang)) {
125                        ls.add(lang);
126                    }
127                }
128                return from(ls);
129            }
130        }
131
132        @Override
133        public LanguageSet merge(final LanguageSet other) {
134            if (other == NO_LANGUAGES) {
135                return this;
136            } else if (other == ANY_LANGUAGE) {
137                return other;
138            } else {
139                final SomeLanguages sl = (SomeLanguages) other;
140                final Set<String> ls = new HashSet<>(languages);
141                for (final String lang : sl.languages) {
142                  ls.add(lang);
143                }
144                return from(ls);
145            }
146        }
147
148        @Override
149        public String toString() {
150            return "Languages(" + languages.toString() + ")";
151        }
152
153    }
154
155    public static final String ANY = "any";
156
157    private static final Map<NameType, Languages> LANGUAGES = new EnumMap<>(NameType.class);
158
159    static {
160        for (final NameType s : NameType.values()) {
161            LANGUAGES.put(s, getInstance(langResourceName(s)));
162        }
163    }
164
165    public static Languages getInstance(final NameType nameType) {
166        return LANGUAGES.get(nameType);
167    }
168
169    public static Languages getInstance(final String languagesResourceName) {
170        // read languages list
171        final Set<String> ls = new HashSet<>();
172        final InputStream langIS = Languages.class.getClassLoader().getResourceAsStream(languagesResourceName);
173
174        if (langIS == null) {
175            throw new IllegalArgumentException("Unable to resolve required resource: " + languagesResourceName);
176        }
177
178        try (final Scanner lsScanner = new Scanner(langIS, ResourceConstants.ENCODING)) {
179            boolean inExtendedComment = false;
180            while (lsScanner.hasNextLine()) {
181                final String line = lsScanner.nextLine().trim();
182                if (inExtendedComment) {
183                    if (line.endsWith(ResourceConstants.EXT_CMT_END)) {
184                        inExtendedComment = false;
185                    }
186                } else {
187                    if (line.startsWith(ResourceConstants.EXT_CMT_START)) {
188                        inExtendedComment = true;
189                    } else if (line.length() > 0) {
190                        ls.add(line);
191                    }
192                }
193            }
194        }
195
196        return new Languages(Collections.unmodifiableSet(ls));
197    }
198
199    private static String langResourceName(final NameType nameType) {
200        return String.format("org/apache/commons/codec/language/bm/%s_languages.txt", nameType.getName());
201    }
202
203    private final Set<String> languages;
204
205    /**
206     * No languages at all.
207     */
208    public static final LanguageSet NO_LANGUAGES = new LanguageSet() {
209        @Override
210        public boolean contains(final String language) {
211            return false;
212        }
213
214        @Override
215        public String getAny() {
216            throw new NoSuchElementException("Can't fetch any language from the empty language set.");
217        }
218
219        @Override
220        public boolean isEmpty() {
221            return true;
222        }
223
224        @Override
225        public boolean isSingleton() {
226            return false;
227        }
228
229        @Override
230        public LanguageSet restrictTo(final LanguageSet other) {
231            return this;
232        }
233
234        @Override
235        public LanguageSet merge(final LanguageSet other) {
236            return other;
237        }
238
239        @Override
240        public String toString() {
241            return "NO_LANGUAGES";
242        }
243    };
244
245    /**
246     * Any/all languages.
247     */
248    public static final LanguageSet ANY_LANGUAGE = new LanguageSet() {
249        @Override
250        public boolean contains(final String language) {
251            return true;
252        }
253
254        @Override
255        public String getAny() {
256            throw new NoSuchElementException("Can't fetch any language from the any language set.");
257        }
258
259        @Override
260        public boolean isEmpty() {
261            return false;
262        }
263
264        @Override
265        public boolean isSingleton() {
266            return false;
267        }
268
269        @Override
270        public LanguageSet restrictTo(final LanguageSet other) {
271            return other;
272        }
273
274        @Override
275        public LanguageSet merge(final LanguageSet other) {
276            return other;
277        }
278
279        @Override
280        public String toString() {
281            return "ANY_LANGUAGE";
282        }
283    };
284
285    private Languages(final Set<String> languages) {
286        this.languages = languages;
287    }
288
289    public Set<String> getLanguages() {
290        return this.languages;
291    }
292}