001/*******************************************************************************
002 * Copyright 2018 The MIT Internet Trust Consortium
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *   http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 *******************************************************************************/
016
017package org.mitre.openid.connect.config;
018
019import java.io.File;
020import java.io.FileNotFoundException;
021import java.io.IOException;
022import java.io.InputStreamReader;
023import java.text.MessageFormat;
024import java.util.ArrayList;
025import java.util.HashMap;
026import java.util.Iterator;
027import java.util.List;
028import java.util.Locale;
029import java.util.Map;
030
031import org.slf4j.Logger;
032import org.slf4j.LoggerFactory;
033import org.springframework.beans.factory.annotation.Autowired;
034import org.springframework.context.support.AbstractMessageSource;
035import org.springframework.core.io.Resource;
036
037import com.google.common.base.Splitter;
038import com.google.gson.JsonElement;
039import com.google.gson.JsonIOException;
040import com.google.gson.JsonObject;
041import com.google.gson.JsonParser;
042import com.google.gson.JsonSyntaxException;
043
044/**
045 * @author jricher
046 */
047public class JsonMessageSource extends AbstractMessageSource {
048
049        private static final Logger logger = LoggerFactory.getLogger(JsonMessageSource.class);
050
051        private Resource baseDirectory;
052
053        private Locale fallbackLocale = new Locale("en"); // US English is the fallback language
054
055        private Map<Locale, List<JsonObject>> languageMaps = new HashMap<>();
056
057        @Autowired
058        private ConfigurationPropertiesBean config;
059
060        @Override
061        protected MessageFormat resolveCode(String code, Locale locale) {
062
063                List<JsonObject> langs = getLanguageMap(locale);
064
065                String value = getValue(code, langs);
066
067                if (value == null) {
068                        // if we haven't found anything, try the default locale
069                        langs = getLanguageMap(fallbackLocale);
070                        value = getValue(code, langs);
071                }
072
073                if (value == null) {
074                        // if it's still null, return null
075                        return null;
076                } else {
077                        // otherwise format the message
078                        return new MessageFormat(value, locale);
079                }
080
081        }
082
083        /**
084         * Get a value from the set of maps, taking the first match in order
085         * @param code
086         * @param langs
087         * @return
088         */
089        private String getValue(String code, List<JsonObject> langs) {
090                if (langs == null || langs.isEmpty()) {
091                        // no language maps, nothing to look up
092                        return null;
093                }
094
095                for (JsonObject lang : langs) {
096                        String value = getValue(code, lang);
097                        if (value != null) {
098                                // short circuit out of here if we find a match, otherwise keep going through the list
099                                return value;
100                        }
101                }
102
103                // if we didn't find anything return null
104                return null;
105        }
106
107        /**
108         * Get a value from a single map
109         * @param code
110         * @param lang
111         * @return
112         */
113        private String getValue(String code, JsonObject lang) {
114
115                // if there's no language map, nothing to look up
116                if (lang == null) {
117                        return null;
118                }
119
120                JsonElement e = lang;
121
122                Iterable<String> parts = Splitter.on('.').split(code);
123                Iterator<String> it = parts.iterator();
124
125                String value = null;
126
127                while (it.hasNext()) {
128                        String p = it.next();
129                        if (e.isJsonObject()) {
130                                JsonObject o = e.getAsJsonObject();
131                                if (o.has(p)) {
132                                        e = o.get(p); // found the next level
133                                        if (!it.hasNext()) {
134                                                // we've reached a leaf, grab it
135                                                if (e.isJsonPrimitive()) {
136                                                        value = e.getAsString();
137                                                }
138                                        }
139                                } else {
140                                        // didn't find it, stop processing
141                                        break;
142                                }
143                        } else {
144                                // didn't find it, stop processing
145                                break;
146                        }
147                }
148
149                return value;
150        }
151
152        /**
153         * @param locale
154         * @return
155         */
156        private List<JsonObject> getLanguageMap(Locale locale) {
157
158                if (!languageMaps.containsKey(locale)) {
159                        try {
160                                List<JsonObject> set = new ArrayList<>();
161                                for (String namespace : config.getLanguageNamespaces()) {
162                                        // full locale string, e.g. "en_US"
163                                        String filename = locale.getLanguage() + "_" + locale.getCountry() + File.separator + namespace + ".json";
164
165                                        Resource r = getBaseDirectory().createRelative(filename);
166
167                                        if (!r.exists()) {
168                                                // fallback to language only
169                                                logger.debug("Fallback locale to language only.");
170                                                filename = locale.getLanguage() + File.separator + namespace + ".json";
171                                                r = getBaseDirectory().createRelative(filename);
172                                        }
173
174                                        logger.info("No locale loaded, trying to load from {}", r);
175
176                                        JsonParser parser = new JsonParser();
177                                        JsonObject obj = (JsonObject) parser.parse(new InputStreamReader(r.getInputStream(), "UTF-8"));
178
179                                        set.add(obj);
180                                }
181                                languageMaps.put(locale, set);
182                        } catch (FileNotFoundException e) {
183                                logger.info("Unable to load locale because no messages file was found for locale {}", locale.getDisplayName());
184                                languageMaps.put(locale, null);
185                        } catch (JsonIOException | JsonSyntaxException | IOException e) {
186                                logger.error("Unable to load locale", e);
187                        }
188                }
189
190                return languageMaps.get(locale);
191        }
192
193        /**
194         * @return the baseDirectory
195         */
196        public Resource getBaseDirectory() {
197                return baseDirectory;
198        }
199
200        /**
201         * @param baseDirectory the baseDirectory to set
202         */
203        public void setBaseDirectory(Resource baseDirectory) {
204                this.baseDirectory = baseDirectory;
205        }
206
207}