| /* |
| * Licensed to the Apache Software Foundation (ASF) under one |
| * or more contributor license agreements. See the NOTICE file |
| * distributed with this work for additional information |
| * regarding copyright ownership. The ASF licenses this file |
| * to you under the Apache License, Version 2.0 (the |
| * "License"); you may not use this file except in compliance |
| * with the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, |
| * software distributed under the License is distributed on an |
| * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| * KIND, either express or implied. See the License for the |
| * specific language governing permissions and limitations |
| * under the License. |
| */ |
| |
| package org.apache.guacamole.extension; |
| |
| import java.io.IOException; |
| import java.io.InputStream; |
| import java.util.Collections; |
| import java.util.HashMap; |
| import java.util.Iterator; |
| import java.util.Map; |
| import java.util.Set; |
| import java.util.regex.Matcher; |
| import java.util.regex.Pattern; |
| import javax.servlet.ServletContext; |
| import org.codehaus.jackson.JsonNode; |
| import org.codehaus.jackson.map.ObjectMapper; |
| import org.codehaus.jackson.node.JsonNodeFactory; |
| import org.codehaus.jackson.node.ObjectNode; |
| import org.apache.guacamole.GuacamoleException; |
| import org.apache.guacamole.environment.Environment; |
| import org.apache.guacamole.properties.StringSetProperty; |
| import org.apache.guacamole.resource.ByteArrayResource; |
| import org.apache.guacamole.resource.Resource; |
| import org.apache.guacamole.resource.WebApplicationResource; |
| import org.slf4j.Logger; |
| import org.slf4j.LoggerFactory; |
| |
| /** |
| * Service which provides access to all built-in languages as resources, and |
| * allows other resources to be added or overlaid against existing resources. |
| */ |
| public class LanguageResourceService { |
| |
| /** |
| * Logger for this class. |
| */ |
| private final Logger logger = LoggerFactory.getLogger(LanguageResourceService.class); |
| |
| /** |
| * The path to the translation folder within the webapp. |
| */ |
| private static final String TRANSLATION_PATH = "/translations"; |
| |
| /** |
| * The JSON property for the human readable display name. |
| */ |
| private static final String LANGUAGE_DISPLAY_NAME_KEY = "NAME"; |
| |
| /** |
| * The Jackson parser for parsing the language JSON files. |
| */ |
| private static final ObjectMapper mapper = new ObjectMapper(); |
| |
| /** |
| * The regular expression to use for parsing the language key from the |
| * filename. |
| */ |
| private static final Pattern LANGUAGE_KEY_PATTERN = Pattern.compile(".*/([a-z]+(_[A-Z]+)?)\\.json"); |
| |
| /** |
| * Comma-separated list of all allowed languages, where each language is |
| * represented by a language key, such as "en" or "en_US". If specified, |
| * only languages within this list will be listed as available by the REST |
| * service. |
| */ |
| public final StringSetProperty ALLOWED_LANGUAGES = new StringSetProperty() { |
| |
| @Override |
| public String getName() { return "allowed-languages"; } |
| |
| }; |
| |
| /** |
| * The set of all language keys which are explicitly listed as allowed |
| * within guacamole.properties, or null if all defined languages should be |
| * allowed. |
| */ |
| private final Set<String> allowedLanguages; |
| |
| /** |
| * Map of all language resources by language key. Language keys are |
| * language and country code pairs, separated by an underscore, like |
| * "en_US". The country code and underscore SHOULD be omitted in the case |
| * that only one dialect of that language is defined, or in the case of the |
| * most universal or well-supported of all supported dialects of that |
| * language. |
| */ |
| private final Map<String, Resource> resources = new HashMap<String, Resource>(); |
| |
| /** |
| * Creates a new service for tracking and parsing available translations |
| * which reads its configuration from the given environment. |
| * |
| * @param environment |
| * The environment from which the configuration properties of this |
| * service should be read. |
| */ |
| public LanguageResourceService(Environment environment) { |
| |
| Set<String> parsedAllowedLanguages; |
| |
| // Parse list of available languages from properties |
| try { |
| parsedAllowedLanguages = environment.getProperty(ALLOWED_LANGUAGES); |
| logger.debug("Available languages will be restricted to: {}", parsedAllowedLanguages); |
| } |
| |
| // Warn of failure to parse |
| catch (GuacamoleException e) { |
| parsedAllowedLanguages = null; |
| logger.error("Unable to parse list of allowed languages: {}", e.getMessage()); |
| logger.debug("Error parsing list of allowed languages.", e); |
| } |
| |
| this.allowedLanguages = parsedAllowedLanguages; |
| |
| } |
| |
| /** |
| * Derives a language key from the filename within the given path, if |
| * possible. If the filename is not a valid language key, null is returned. |
| * |
| * @param path |
| * The path containing the filename to derive the language key from. |
| * |
| * @return |
| * The derived language key, or null if the filename is not a valid |
| * language key. |
| */ |
| public String getLanguageKey(String path) { |
| |
| // Parse language key from filename |
| Matcher languageKeyMatcher = LANGUAGE_KEY_PATTERN.matcher(path); |
| if (!languageKeyMatcher.matches()) |
| return null; |
| |
| // Return parsed key |
| return languageKeyMatcher.group(1); |
| |
| } |
| |
| /** |
| * Merges the given JSON objects. Any leaf node in overlay will overwrite |
| * the corresponding path in original. |
| * |
| * @param original |
| * The original JSON object to which changes should be applied. |
| * |
| * @param overlay |
| * The JSON object containing changes that should be applied. |
| * |
| * @return |
| * The newly constructed JSON object that is the result of merging |
| * original and overlay. |
| */ |
| private JsonNode mergeTranslations(JsonNode original, JsonNode overlay) { |
| |
| // If we are at a leaf node, the result of merging is simply the overlay |
| if (!overlay.isObject() || original == null) |
| return overlay; |
| |
| // Create mutable copy of original |
| ObjectNode newNode = JsonNodeFactory.instance.objectNode(); |
| Iterator<String> fieldNames = original.getFieldNames(); |
| while (fieldNames.hasNext()) { |
| String fieldName = fieldNames.next(); |
| newNode.put(fieldName, original.get(fieldName)); |
| } |
| |
| // Merge each field |
| fieldNames = overlay.getFieldNames(); |
| while (fieldNames.hasNext()) { |
| String fieldName = fieldNames.next(); |
| newNode.put(fieldName, mergeTranslations(original.get(fieldName), overlay.get(fieldName))); |
| } |
| |
| return newNode; |
| |
| } |
| |
| /** |
| * Parses the given language resource, returning the resulting JsonNode. |
| * If the resource cannot be read because it does not exist, null is |
| * returned. |
| * |
| * @param resource |
| * The language resource to parse. Language resources must have the |
| * mimetype "application/json". |
| * |
| * @return |
| * A JsonNode representing the root of the parsed JSON tree, or null if |
| * the given resource does not exist. |
| * |
| * @throws IOException |
| * If an error occurs while parsing the resource as JSON. |
| */ |
| private JsonNode parseLanguageResource(Resource resource) throws IOException { |
| |
| // Get resource stream |
| InputStream stream = resource.asStream(); |
| if (stream == null) |
| return null; |
| |
| // Parse JSON tree |
| try { |
| JsonNode tree = mapper.readTree(stream); |
| return tree; |
| } |
| |
| // Ensure stream is always closed |
| finally { |
| stream.close(); |
| } |
| |
| } |
| |
| /** |
| * Returns whether a language having the given key should be allowed to be |
| * loaded. If language availability restrictions are imposed through |
| * guacamole.properties, this may return false in some cases. By default, |
| * this function will always return true. Note that just because a language |
| * key is allowed to be loaded does not imply that the language key is |
| * valid. |
| * |
| * @param languageKey |
| * The language key of the language to test. |
| * |
| * @return |
| * true if the given language key should be allowed to be loaded, false |
| * otherwise. |
| */ |
| private boolean isLanguageAllowed(String languageKey) { |
| |
| // If no list is provided, all languages are implicitly available |
| if (allowedLanguages == null) |
| return true; |
| |
| return allowedLanguages.contains(languageKey); |
| |
| } |
| |
| /** |
| * Adds or overlays the given language resource, which need not exist in |
| * the ServletContext. If a language resource is already defined for the |
| * given language key, the strings from the given resource will be overlaid |
| * on top of the existing strings, augmenting or overriding the available |
| * strings for that language. |
| * |
| * @param key |
| * The language key of the resource being added. Language keys are |
| * pairs consisting of a language code followed by an underscore and |
| * country code, such as "en_US". |
| * |
| * @param resource |
| * The language resource to add. This resource must have the mimetype |
| * "application/json". |
| */ |
| public void addLanguageResource(String key, Resource resource) { |
| |
| // Skip loading of language if not allowed |
| if (!isLanguageAllowed(key)) { |
| logger.debug("OMITTING language: \"{}\"", key); |
| return; |
| } |
| |
| // Merge language resources if already defined |
| Resource existing = resources.get(key); |
| if (existing != null) { |
| |
| try { |
| |
| // Read the original language resource |
| JsonNode existingTree = parseLanguageResource(existing); |
| if (existingTree == null) { |
| logger.warn("Base language resource \"{}\" does not exist.", key); |
| return; |
| } |
| |
| // Read new language resource |
| JsonNode resourceTree = parseLanguageResource(resource); |
| if (resourceTree == null) { |
| logger.warn("Overlay language resource \"{}\" does not exist.", key); |
| return; |
| } |
| |
| // Merge the language resources |
| JsonNode mergedTree = mergeTranslations(existingTree, resourceTree); |
| resources.put(key, new ByteArrayResource("application/json", mapper.writeValueAsBytes(mergedTree))); |
| |
| logger.debug("Merged strings with existing language: \"{}\"", key); |
| |
| } |
| catch (IOException e) { |
| logger.error("Unable to merge language resource \"{}\": {}", key, e.getMessage()); |
| logger.debug("Error merging language resource.", e); |
| } |
| |
| } |
| |
| // Otherwise, add new language resource |
| else { |
| resources.put(key, resource); |
| logger.debug("Added language: \"{}\"", key); |
| } |
| |
| } |
| |
| /** |
| * Adds or overlays all languages defined within the /translations |
| * directory of the given ServletContext. If no such language files exist, |
| * nothing is done. If a language is already defined, the strings from the |
| * will be overlaid on top of the existing strings, augmenting or |
| * overriding the available strings for that language. The language key |
| * for each language file is derived from the filename. |
| * |
| * @param context |
| * The ServletContext from which language files should be loaded. |
| */ |
| public void addLanguageResources(ServletContext context) { |
| |
| // Get the paths of all the translation files |
| Set<?> resourcePaths = context.getResourcePaths(TRANSLATION_PATH); |
| |
| // If no translation files found, nothing to add |
| if (resourcePaths == null) |
| return; |
| |
| // Iterate through all the found language files and add them to the map |
| for (Object resourcePathObject : resourcePaths) { |
| |
| // Each resource path is guaranteed to be a string |
| String resourcePath = (String) resourcePathObject; |
| |
| // Parse language key from path |
| String languageKey = getLanguageKey(resourcePath); |
| if (languageKey == null) { |
| logger.warn("Invalid language file name: \"{}\"", resourcePath); |
| continue; |
| } |
| |
| // Add/overlay new resource |
| addLanguageResource( |
| languageKey, |
| new WebApplicationResource(context, "application/json", resourcePath) |
| ); |
| |
| } |
| |
| } |
| |
| /** |
| * Returns a set of all unique language keys currently associated with |
| * language resources stored in this service. The returned set cannot be |
| * modified. |
| * |
| * @return |
| * A set of all unique language keys currently associated with this |
| * service. |
| */ |
| public Set<String> getLanguageKeys() { |
| return Collections.unmodifiableSet(resources.keySet()); |
| } |
| |
| /** |
| * Returns a map of all languages currently associated with this service, |
| * where the key of each map entry is the language key. The returned map |
| * cannot be modified. |
| * |
| * @return |
| * A map of all languages currently associated with this service. |
| */ |
| public Map<String, Resource> getLanguageResources() { |
| return Collections.unmodifiableMap(resources); |
| } |
| |
| /** |
| * Returns a mapping of all language keys to their corresponding human- |
| * readable language names. If an error occurs while parsing a language |
| * resource, its key/name pair will simply be omitted. The returned map |
| * cannot be modified. |
| * |
| * @return |
| * A map of all language keys and their corresponding human-readable |
| * names. |
| */ |
| public Map<String, String> getLanguageNames() { |
| |
| Map<String, String> languageNames = new HashMap<String, String>(); |
| |
| // For each language key/resource pair |
| for (Map.Entry<String, Resource> entry : resources.entrySet()) { |
| |
| // Get language key and resource |
| String languageKey = entry.getKey(); |
| Resource resource = entry.getValue(); |
| |
| // Get stream for resource |
| InputStream resourceStream = resource.asStream(); |
| if (resourceStream == null) { |
| logger.warn("Expected language resource does not exist: \"{}\".", languageKey); |
| continue; |
| } |
| |
| // Get name node of language |
| try { |
| JsonNode tree = mapper.readTree(resourceStream); |
| JsonNode nameNode = tree.get(LANGUAGE_DISPLAY_NAME_KEY); |
| |
| // Attempt to read language name from node |
| String languageName; |
| if (nameNode == null || (languageName = nameNode.getTextValue()) == null) { |
| logger.warn("Root-level \"" + LANGUAGE_DISPLAY_NAME_KEY + "\" string missing or invalid in language \"{}\"", languageKey); |
| languageName = languageKey; |
| } |
| |
| // Add language key/name pair to map |
| languageNames.put(languageKey, languageName); |
| |
| } |
| |
| // Continue with next language if unable to read |
| catch (IOException e) { |
| logger.warn("Unable to read language resource \"{}\".", languageKey); |
| logger.debug("Error reading language resource.", e); |
| } |
| |
| } |
| |
| return Collections.unmodifiableMap(languageNames); |
| |
| } |
| |
| } |