diff --git a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/extension/ExtensionModule.java b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/extension/ExtensionModule.java index eb962b09f..4bb966279 100644 --- a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/extension/ExtensionModule.java +++ b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/extension/ExtensionModule.java @@ -95,6 +95,11 @@ public class ExtensionModule extends ServletModule { */ private Class boundAuthenticationProvider = null; + /** + * Service for adding and retrieving language resources. + */ + private final LanguageResourceService languageResourceService = new LanguageResourceService(); + /** * Returns the classloader that should be used as the parent classloader * for all extensions. If the GUACAMOLE_HOME/lib directory exists, this @@ -308,6 +313,12 @@ public class ExtensionModule extends ServletModule { @Override protected void configureServlets() { + // Bind language resource service + bind(LanguageResourceService.class).toInstance(languageResourceService); + + // Load initial language resources from servlet context + languageResourceService.addLanguageResources(getServletContext()); + // Load authentication provider from guacamole.properties for sake of backwards compatibility Class authProviderProperty = getAuthProviderProperty(); if (authProviderProperty != null) diff --git a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/extension/LanguageResourceService.java b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/extension/LanguageResourceService.java new file mode 100644 index 000000000..d9f27292c --- /dev/null +++ b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/extension/LanguageResourceService.java @@ -0,0 +1,251 @@ +/* + * Copyright (C) 2015 Glyptodon LLC + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package org.glyptodon.guacamole.net.basic.extension; + +import java.io.IOException; +import java.io.InputStream; +import java.util.Collections; +import java.util.HashMap; +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.glyptodon.guacamole.net.basic.resource.Resource; +import org.glyptodon.guacamole.net.basic.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. + * + * @author Michael Jumper + */ +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"); + + /** + * Map of all language resources by language key. Language keys are + * language and country code pairs, separated by an underscore, like + * "en_US". + */ + private final Map resources = new HashMap(); + + /** + * 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); + + } + + /** + * 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) { + 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 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 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 getLanguageNames() { + + Map languageNames = new HashMap(); + + // For each language key/resource pair + for (Map.Entry 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); + + } + +} diff --git a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/language/LanguageRESTService.java b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/language/LanguageRESTService.java index 80917a395..e1a35f89d 100644 --- a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/language/LanguageRESTService.java +++ b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/language/LanguageRESTService.java @@ -22,27 +22,13 @@ package org.glyptodon.guacamole.net.basic.rest.language; -import java.io.IOException; -import java.io.InputStream; -import java.util.Collections; -import java.util.HashMap; +import com.google.inject.Inject; import java.util.Map; -import java.util.Set; -import java.util.regex.Matcher; -import java.util.regex.Pattern; -import javax.servlet.ServletContext; import javax.ws.rs.GET; import javax.ws.rs.Path; import javax.ws.rs.Produces; -import javax.ws.rs.QueryParam; -import javax.ws.rs.core.Context; import javax.ws.rs.core.MediaType; -import org.codehaus.jackson.JsonNode; -import org.codehaus.jackson.map.ObjectMapper; -import org.glyptodon.guacamole.GuacamoleException; -import org.glyptodon.guacamole.net.basic.rest.AuthProviderRESTExposure; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import org.glyptodon.guacamole.net.basic.extension.LanguageResourceService; /** @@ -55,108 +41,23 @@ import org.slf4j.LoggerFactory; public class LanguageRESTService { /** - * Logger for this class. + * Service for retrieving information regarding available language + * resources. */ - private final Logger logger = LoggerFactory.getLogger(LanguageRESTService.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"); + @Inject + private LanguageResourceService languageResourceService; /** * Returns a map of all available language keys to their corresponding * human-readable names. * - * @param authToken - * The authentication token that is used to authenticate the user - * performing the operation. - * - * @param servletContext - * The ServletContext associated with the request. - * * @return * A map of languages defined in the system, of language key to * display name. - * - * @throws GuacamoleException - * If an error occurs while retrieving the available languages. */ @GET - @AuthProviderRESTExposure - public Map getLanguages(@QueryParam("token") String authToken, - @Context ServletContext servletContext) throws GuacamoleException { - - // Get the paths of all the translation files - Set resourcePaths = servletContext.getResourcePaths(TRANSLATION_PATH); - - // If no translation files found, return an empty map - if (resourcePaths == null) - return Collections.emptyMap(); - - Map languageMap = new HashMap(); - - // Iterate through all the found language files and add them to the return map - for (Object resourcePathObject : resourcePaths) { - - // Each resource path is guaranteed to be a string - String resourcePath = (String) resourcePathObject; - - // Get input stream for language file - InputStream languageFileStream = servletContext.getResourceAsStream(resourcePath); - if (languageFileStream == null) { - logger.warn("Unable to read language resource \"{}\"", resourcePath); - continue; - } - - try { - - // Parse language key from filename - String languageKey; - Matcher languageKeyMatcher = LANGUAGE_KEY_PATTERN.matcher(resourcePath); - if (!languageKeyMatcher.matches() || (languageKey = languageKeyMatcher.group(1)) == null) { - logger.warn("Invalid language file name: \"{}\"", resourcePath); - continue; - } - - // Get name node of language - JsonNode tree = mapper.readTree(languageFileStream); - 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 file \"{}\"", resourcePath); - languageName = languageKey; - } - - // Add language key/name pair to map - languageMap.put(languageKey, languageName); - - } - catch (IOException e) { - logger.warn("Unable to read language resource \"{}\": {}", resourcePath, e.getMessage()); - logger.debug("Error while reading language resource.", e); - } - } - - return languageMap; + public Map getLanguages() { + return languageResourceService.getLanguageNames(); } - -} \ No newline at end of file + +}