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}