diff --git a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/RESTServletModule.java b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/RESTServletModule.java index 91cade593..e87ac7e54 100644 --- a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/RESTServletModule.java +++ b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/RESTServletModule.java @@ -32,6 +32,7 @@ import org.glyptodon.guacamole.net.basic.rest.connection.ConnectionRESTService; import org.glyptodon.guacamole.net.basic.rest.connectiongroup.ConnectionGroupRESTService; import org.glyptodon.guacamole.net.basic.rest.protocol.ProtocolRESTService; import org.glyptodon.guacamole.net.basic.rest.activeconnection.ActiveConnectionRESTService; +import org.glyptodon.guacamole.net.basic.rest.language.LanguageRESTService; import org.glyptodon.guacamole.net.basic.rest.user.UserRESTService; /** @@ -45,13 +46,14 @@ public class RESTServletModule extends ServletModule { protected void configureServlets() { // Set up the API endpoints - bind(ClipboardRESTService.class); - bind(ConnectionRESTService.class); - bind(ConnectionGroupRESTService.class); - bind(ProtocolRESTService.class); - bind(UserRESTService.class); - bind(TokenRESTService.class); bind(ActiveConnectionRESTService.class); + bind(ClipboardRESTService.class); + bind(ConnectionGroupRESTService.class); + bind(ConnectionRESTService.class); + bind(LanguageRESTService.class); + bind(ProtocolRESTService.class); + bind(TokenRESTService.class); + bind(UserRESTService.class); // Set up the servlet and JSON mappings bind(GuiceContainer.class); diff --git a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/activeconnection/ActiveConnectionRESTService.java b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/activeconnection/ActiveConnectionRESTService.java index 1a47582bc..107f97ef8 100644 --- a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/activeconnection/ActiveConnectionRESTService.java +++ b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/activeconnection/ActiveConnectionRESTService.java @@ -147,7 +147,6 @@ public class ActiveConnectionRESTService { * If an error occurs while deleting the active connections. */ @PATCH - @Path("/") @AuthProviderRESTExposure public void patchTunnels(@QueryParam("token") String authToken, List> patches) throws GuacamoleException { 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 new file mode 100644 index 000000000..8c1131bb8 --- /dev/null +++ b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/language/LanguageRESTService.java @@ -0,0 +1,159 @@ +/* + * 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.rest.language; + +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 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; + + +/** + * A REST Service for handling the listing of languages. + * + * @author James Muehlner + */ +@Path("/languages") +@Produces(MediaType.APPLICATION_JSON) +public class LanguageRESTService { + + /** + * Logger for this class. + */ + 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"); + + /** + * 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.EMPTY_MAP; + + Map languageMap = new HashMap(); + + // Iterate through all the found language files and add them to the return map + for (String resourcePath : resourcePaths) { + + // 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; + } + +} \ No newline at end of file diff --git a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/language/package-info.java b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/language/package-info.java new file mode 100644 index 000000000..e0e7ad697 --- /dev/null +++ b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/language/package-info.java @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2014 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. + */ + +/** + * Classes related to the language retrieval aspect of the Guacamole REST API. + */ +package org.glyptodon.guacamole.net.basic.rest.language; + diff --git a/guacamole/src/main/webapp/app/rest/services/cacheService.js b/guacamole/src/main/webapp/app/rest/services/cacheService.js index f21dd6388..12ee0f275 100644 --- a/guacamole/src/main/webapp/app/rest/services/cacheService.js +++ b/guacamole/src/main/webapp/app/rest/services/cacheService.js @@ -1,5 +1,5 @@ /* - * Copyright (C) 2014 Glyptodon LLC + * 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 @@ -41,6 +41,13 @@ angular.module('rest').factory('cacheService', ['$injector', */ service.connections = $cacheFactory('API-CONNECTIONS'); + /** + * Cache used by languageService. + * + * @type $cacheFactory.Cache + */ + service.languages = $cacheFactory('API-LANGUAGES'); + /** * Cache used by protocolService. * @@ -59,8 +66,9 @@ angular.module('rest').factory('cacheService', ['$injector', * Clear all caches defined in this service. */ service.clearCaches = function clearCaches() { - service.protocols.removeAll(); service.connections.removeAll(); + service.languages.removeAll(); + service.protocols.removeAll(); service.users.removeAll(); }; diff --git a/guacamole/src/main/webapp/app/rest/services/languageService.js b/guacamole/src/main/webapp/app/rest/services/languageService.js new file mode 100644 index 000000000..407a6a900 --- /dev/null +++ b/guacamole/src/main/webapp/app/rest/services/languageService.js @@ -0,0 +1,64 @@ +/* + * 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. + */ + +/** + * Service for operating on language metadata via the REST API. + */ +angular.module('rest').factory('languageService', ['$injector', + function languageService($injector) { + + // Required services + var $http = $injector.get('$http'); + var authenticationService = $injector.get('authenticationService'); + var cacheService = $injector.get('cacheService'); + + var service = {}; + + /** + * Makes a request to the REST API to get the list of languages, returning + * a promise that provides a map of language names by language key if + * successful. + * + * @returns {Promise.>} + * A promise which will resolve with a map of language names by + * language key upon success. + */ + service.getLanguages = function getLanguages() { + + // Build HTTP parameters set + var httpParameters = { + token : authenticationService.getCurrentToken() + }; + + // Retrieve available languages + return $http({ + cache : cacheService.languages, + method : 'GET', + url : 'api/languages', + params : httpParameters + }); + + }; + + return service; + +}]); diff --git a/guacamole/src/main/webapp/app/settings/directives/guacSettingsPreferences.js b/guacamole/src/main/webapp/app/settings/directives/guacSettingsPreferences.js index 4a5e0e194..482529a37 100644 --- a/guacamole/src/main/webapp/app/settings/directives/guacSettingsPreferences.js +++ b/guacamole/src/main/webapp/app/settings/directives/guacSettingsPreferences.js @@ -30,8 +30,7 @@ angular.module('settings').directive('guacSettingsPreferences', [function guacSe restrict: 'E', replace: true, - scope: { - }, + scope: {}, templateUrl: 'app/settings/templates/settingsPreferences.html', controller: ['$scope', '$injector', function settingsPreferencesController($scope, $injector) { @@ -40,11 +39,13 @@ angular.module('settings').directive('guacSettingsPreferences', [function guacSe var PermissionSet = $injector.get('PermissionSet'); // Required services + var $translate = $injector.get('$translate'); var authenticationService = $injector.get('authenticationService'); var guacNotification = $injector.get('guacNotification'); - var userService = $injector.get('userService'); + var languageService = $injector.get('languageService'); var permissionService = $injector.get('permissionService'); var preferenceService = $injector.get('preferenceService'); + var userService = $injector.get('userService'); /** * An action to be provided along with the object sent to @@ -71,6 +72,21 @@ angular.module('settings').directive('guacSettingsPreferences', [function guacSe * @type Object. */ $scope.preferences = preferenceService.preferences; + + /** + * A map of all available language keys to their human-readable + * names. + * + * @type Object. + */ + $scope.languages = null; + + /** + * Switches the active display langugae to the chosen language. + */ + $scope.changeLanguage = function changeLanguage() { + $translate.use($scope.preferences.language); + }; /** * The new password for the user. @@ -151,6 +167,12 @@ angular.module('settings').directive('guacSettingsPreferences', [function guacSe }; + // Retrieve defined languages + languageService.getLanguages() + .success(function languagesRetrieved(languages) { + $scope.languages = languages; + }); + // Retrieve current permissions permissionService.getPermissions(username) .success(function permissionsRetrieved(permissions) { @@ -161,6 +183,20 @@ angular.module('settings').directive('guacSettingsPreferences', [function guacSe }); + /** + * Returns whether critical data has completed being loaded. + * + * @returns {Boolean} + * true if enough data has been loaded for the user interface to be + * useful, false otherwise. + */ + $scope.isLoaded = function isLoaded() { + + return $scope.canChangePassword !== null + && $scope.languages !== null; + + }; + }] }; diff --git a/guacamole/src/main/webapp/app/settings/services/preferenceService.js b/guacamole/src/main/webapp/app/settings/services/preferenceService.js index ade3c7228..e1da5a44d 100644 --- a/guacamole/src/main/webapp/app/settings/services/preferenceService.js +++ b/guacamole/src/main/webapp/app/settings/services/preferenceService.js @@ -28,8 +28,9 @@ angular.module('settings').factory('preferenceService', ['$injector', function preferenceService($injector) { // Required services - var $window = $injector.get('$window'); var $rootScope = $injector.get('$rootScope'); + var $translate = $injector.get('$translate'); + var $window = $injector.get('$window'); var service = {}; @@ -95,7 +96,14 @@ angular.module('settings').factory('preferenceService', ['$injector', * * @type String */ - inputMethod : service.inputMethods.NONE + inputMethod : service.inputMethods.NONE, + + /** + * The selected language. + * + * @type String + */ + language : $translate.use() }; diff --git a/guacamole/src/main/webapp/app/settings/styles/update-password.css b/guacamole/src/main/webapp/app/settings/styles/preferences.css similarity index 94% rename from guacamole/src/main/webapp/app/settings/styles/update-password.css rename to guacamole/src/main/webapp/app/settings/styles/preferences.css index 3cf4d6332..eadf49020 100644 --- a/guacamole/src/main/webapp/app/settings/styles/update-password.css +++ b/guacamole/src/main/webapp/app/settings/styles/preferences.css @@ -20,7 +20,8 @@ * THE SOFTWARE. */ -.preferences .update-password .form { +.preferences .update-password .form, +.preferences .language .form { padding-left: 0.5em; border-left: 3px solid rgba(0, 0, 0, 0.125); -} +} \ No newline at end of file diff --git a/guacamole/src/main/webapp/app/settings/templates/settingsPreferences.html b/guacamole/src/main/webapp/app/settings/templates/settingsPreferences.html index d2b0a63cf..525fde30c 100644 --- a/guacamole/src/main/webapp/app/settings/templates/settingsPreferences.html +++ b/guacamole/src/main/webapp/app/settings/templates/settingsPreferences.html @@ -1,4 +1,4 @@ -
+
+ +
+

{{'SETTINGS_PREFERENCES.HELP_LANGUAGE' | translate}}

+ + +
+ + + + + +
{{'SETTINGS_PREFERENCES.FIELD_HEADER_LANGUAGE' | translate}}
+
+
+

{{'SETTINGS_PREFERENCES.HELP_UPDATE_PASSWORD' | translate}}

diff --git a/guacamole/src/main/webapp/translations/en_US.json b/guacamole/src/main/webapp/translations/en_US.json index 3d72affc9..f0c7ab92a 100644 --- a/guacamole/src/main/webapp/translations/en_US.json +++ b/guacamole/src/main/webapp/translations/en_US.json @@ -1,5 +1,7 @@ { - + + "NAME" : "English (US)", + "APP" : { "ACTION_ACKNOWLEDGE" : "OK", @@ -411,6 +413,7 @@ "ERROR_PASSWORD_BLANK" : "Your password cannot be blank.", "ERROR_PASSWORD_MISMATCH" : "@:APP.ERROR_PASSWORD_MISMATCH", + "FIELD_HEADER_LANGUAGE" : "Display language:", "FIELD_HEADER_PASSWORD" : "Password:", "FIELD_HEADER_PASSWORD_OLD" : "Current Password:", "FIELD_HEADER_PASSWORD_NEW" : "New Password:", @@ -422,6 +425,7 @@ "HELP_INPUT_METHOD_NONE" : "@:CLIENT.HELP_INPUT_METHOD_NONE", "HELP_INPUT_METHOD_OSK" : "@:CLIENT.HELP_INPUT_METHOD_OSK", "HELP_INPUT_METHOD_TEXT" : "@:CLIENT.HELP_INPUT_METHOD_TEXT", + "HELP_LANGUAGE" : "Select a different language below to change the language of all text within Guacamole. Available choices will depend on which languages are installed.", "HELP_MOUSE_MODE_ABSOLUTE" : "@:CLIENT.HELP_MOUSE_MODE_ABSOLUTE", "HELP_MOUSE_MODE_RELATIVE" : "@:CLIENT.HELP_MOUSE_MODE_RELATIVE", "HELP_UPDATE_PASSWORD" : "If you wish to change your password, enter your current password and the desired new password below, and click \"Update Password\". The change will take effect immediately.",