001/*******************************************************************************
002 * Copyright 2017 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.IOException;
021import java.io.InputStreamReader;
022import java.text.MessageFormat;
023import java.util.ArrayList;
024import java.util.HashMap;
025import java.util.Iterator;
026import java.util.List;
027import java.util.Locale;
028import java.util.Map;
029
030import org.slf4j.Logger;
031import org.slf4j.LoggerFactory;
032import org.springframework.beans.factory.annotation.Autowired;
033import org.springframework.context.support.AbstractMessageSource;
034import org.springframework.core.io.Resource;
035
036import com.google.common.base.Splitter;
037import com.google.gson.JsonElement;
038import com.google.gson.JsonIOException;
039import com.google.gson.JsonObject;
040import com.google.gson.JsonParser;
041import com.google.gson.JsonSyntaxException;
042
043/**
044 * @author jricher
045 *
046 */
047public class JsonMessageSource extends AbstractMessageSource {
048        // Logger for this class
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 locale
111         * @param lang
112         * @return
113         */
114        private String getValue(String code, JsonObject lang) {
115
116                // if there's no language map, nothing to look up
117                if (lang == null) {
118                        return null;
119                }
120
121                JsonElement e = lang;
122
123                Iterable<String> parts = Splitter.on('.').split(code);
124                Iterator<String> it = parts.iterator();
125
126                String value = null;
127
128                while (it.hasNext()) {
129                        String p = it.next();
130                        if (e.isJsonObject()) {
131                                JsonObject o = e.getAsJsonObject();
132                                if (o.has(p)) {
133                                        e = o.get(p); // found the next level
134                                        if (!it.hasNext()) {
135                                                // we've reached a leaf, grab it
136                                                if (e.isJsonPrimitive()) {
137                                                        value = e.getAsString();
138                                                }
139                                        }
140                                } else {
141                                        // didn't find it, stop processing
142                                        break;
143                                }
144                        } else {
145                                // didn't find it, stop processing
146                                break;
147                        }
148                }
149
150
151                return value;
152
153        }
154
155        /**
156         * @param locale
157         * @return
158         */
159        private List<JsonObject> getLanguageMap(Locale locale) {
160
161                if (!languageMaps.containsKey(locale)) {
162                        try {
163                                List<JsonObject> set = new ArrayList<>();
164                                for (String namespace : config.getLanguageNamespaces()) {
165                                        // full locale string, e.g. "en_US"
166                                        String filename = locale.getLanguage() + "_" + locale.getCountry() + File.separator + namespace + ".json";
167
168                                        Resource r = getBaseDirectory().createRelative(filename);
169
170                                        if (!r.exists()) {
171                                                // fallback to language only
172                                                logger.debug("Fallback locale to language only.");
173                                                filename = locale.getLanguage() + File.separator + namespace + ".json";
174                                                r = getBaseDirectory().createRelative(filename);
175                                        }
176
177                                        logger.info("No locale loaded, trying to load from " + r);
178
179                                        JsonParser parser = new JsonParser();
180                                        JsonObject obj = (JsonObject) parser.parse(new InputStreamReader(r.getInputStream(), "UTF-8"));
181
182                                        set.add(obj);
183                                }
184                                languageMaps.put(locale, set);
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        }
195
196        /**
197         * @return the baseDirectory
198         */
199        public Resource getBaseDirectory() {
200                return baseDirectory;
201        }
202
203        /**
204         * @param baseDirectory the baseDirectory to set
205         */
206        public void setBaseDirectory(Resource baseDirectory) {
207                this.baseDirectory = baseDirectory;
208        }
209
210}