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}