From 30179c405fe2dc856c608df8226e53d81702b6eb Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Mon, 15 Aug 2016 22:24:04 -0700 Subject: [PATCH 1/4] GUACAMOLE-36: Generalize and simplify handling of REST API errors. --- .../org/apache/guacamole/rest/APIError.java | 152 +++++++++--------- .../apache/guacamole/rest/APIException.java | 100 ++---------- .../guacamole/rest/RESTExceptionWrapper.java | 118 ++------------ .../APIConnectionRecordSortPredicate.java | 7 +- 4 files changed, 112 insertions(+), 265 deletions(-) diff --git a/guacamole/src/main/java/org/apache/guacamole/rest/APIError.java b/guacamole/src/main/java/org/apache/guacamole/rest/APIError.java index c6a2c635c..7727b85b0 100644 --- a/guacamole/src/main/java/org/apache/guacamole/rest/APIError.java +++ b/guacamole/src/main/java/org/apache/guacamole/rest/APIError.java @@ -20,8 +20,15 @@ package org.apache.guacamole.rest; import java.util.Collection; -import javax.ws.rs.core.Response; +import org.apache.guacamole.GuacamoleClientException; +import org.apache.guacamole.GuacamoleException; +import org.apache.guacamole.GuacamoleResourceNotFoundException; +import org.apache.guacamole.GuacamoleSecurityException; import org.apache.guacamole.form.Field; +import org.apache.guacamole.net.auth.credentials.GuacamoleCredentialsException; +import org.apache.guacamole.net.auth.credentials.GuacamoleInsufficientCredentialsException; +import org.apache.guacamole.net.auth.credentials.GuacamoleInvalidCredentialsException; +import org.apache.guacamole.tunnel.GuacamoleStreamException; /** * Describes an error that occurred within a REST endpoint. @@ -60,124 +67,115 @@ public class APIError { * The requested operation could not be performed because the request * itself was malformed. */ - BAD_REQUEST(Response.Status.BAD_REQUEST), + BAD_REQUEST, /** * The credentials provided were invalid. */ - INVALID_CREDENTIALS(Response.Status.FORBIDDEN), + INVALID_CREDENTIALS, /** * The credentials provided were not necessarily invalid, but were not * sufficient to determine validity. */ - INSUFFICIENT_CREDENTIALS(Response.Status.FORBIDDEN), + INSUFFICIENT_CREDENTIALS, /** * An internal server error has occurred. */ - INTERNAL_ERROR(Response.Status.INTERNAL_SERVER_ERROR), + INTERNAL_ERROR, /** * An object related to the request does not exist. */ - NOT_FOUND(Response.Status.NOT_FOUND), + NOT_FOUND, /** * Permission was denied to perform the requested operation. */ - PERMISSION_DENIED(Response.Status.FORBIDDEN), + PERMISSION_DENIED, /** * An error occurred within an intercepted stream, terminating that * stream. The Guacamole protocol status code of that error can be * retrieved with getStatusCode(). */ - STREAM_ERROR(Response.Status.BAD_REQUEST); + STREAM_ERROR; /** - * The HTTP status associated with this error type. - */ - private final Response.Status status; - - /** - * Defines a new error type associated with the given HTTP status. + * Returns the REST API error type which corresponds to the type of the + * given exception. * - * @param status - * The HTTP status to associate with the error type. - */ - Type(Response.Status status) { - this.status = status; - } - - /** - * Returns the HTTP status associated with this error type. + * @param exception + * The exception to use to derive the API error type. * * @return - * The HTTP status associated with this error type. + * The API error type which corresponds to the type of the given + * exception. */ - public Response.Status getStatus() { - return status; + public static Type fromGuacamoleException(GuacamoleException exception) { + + // Additional credentials are needed + if (exception instanceof GuacamoleInsufficientCredentialsException) + return INSUFFICIENT_CREDENTIALS; + + // The provided credentials are wrong + if (exception instanceof GuacamoleInvalidCredentialsException) + return INVALID_CREDENTIALS; + + // Generic permission denied + if (exception instanceof GuacamoleSecurityException) + return PERMISSION_DENIED; + + // Arbitrary resource not found + if (exception instanceof GuacamoleResourceNotFoundException) + return NOT_FOUND; + + // Arbitrary bad requests + if (exception instanceof GuacamoleClientException) + return BAD_REQUEST; + + // Errors from intercepted streams + if (exception instanceof GuacamoleStreamException) + return STREAM_ERROR; + + // All other errors + return INTERNAL_ERROR; + } } /** - * Creates a new APIError of type STREAM_ERROR and having the given - * Guacamole protocol status code and human-readable message. The status - * code and message should be taken directly from the "ack" instruction - * causing the error. + * Creates a new APIError which exposes the details of the given + * GuacamoleException. * - * @param statusCode - * The Guacamole protocol status code describing the error that - * occurred within the intercepted stream. - * - * @param message - * An arbitrary human-readable message describing the error that - * occurred. + * @param exception + * The GuacamoleException from which the details of the new APIError + * should be derived. */ - public APIError(int statusCode, String message) { - this.type = Type.STREAM_ERROR; - this.message = message; - this.statusCode = statusCode; - this.expected = null; - } + public APIError(GuacamoleException exception) { - /** - * Create a new APIError with the specified error message. - * - * @param type - * The type of error that occurred. - * - * @param message - * The error message. - */ - public APIError(Type type, String message) { - this.type = type; - this.message = message; - this.statusCode = null; - this.expected = null; - } + // Build base REST service error + this.type = Type.fromGuacamoleException(exception); + this.message = exception.getMessage(); + + // Add expected credentials if applicable + if (exception instanceof GuacamoleCredentialsException) { + GuacamoleCredentialsException credentialsException = (GuacamoleCredentialsException) exception; + this.expected = credentialsException.getCredentialsInfo().getFields(); + } + else + this.expected = null; + + // Add stream status code if applicable + if (exception instanceof GuacamoleStreamException) { + GuacamoleStreamException streamException = (GuacamoleStreamException) exception; + this.statusCode = streamException.getStatus().getGuacamoleStatusCode(); + } + else + this.statusCode = null; - /** - * Create a new APIError with the specified error message and parameter - * information. - * - * @param type - * The type of error that occurred. - * - * @param message - * The error message. - * - * @param expected - * All parameters expected in the original request, or now required as - * a result of the original request, as a collection of fields. - */ - public APIError(Type type, String message, Collection expected) { - this.type = type; - this.message = message; - this.statusCode = null; - this.expected = expected; } /** diff --git a/guacamole/src/main/java/org/apache/guacamole/rest/APIException.java b/guacamole/src/main/java/org/apache/guacamole/rest/APIException.java index ea4cf5d5d..0e1a4f54a 100644 --- a/guacamole/src/main/java/org/apache/guacamole/rest/APIException.java +++ b/guacamole/src/main/java/org/apache/guacamole/rest/APIException.java @@ -19,17 +19,16 @@ package org.apache.guacamole.rest; -import java.util.Collection; import javax.ws.rs.WebApplicationException; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; -import org.apache.guacamole.form.Field; -import org.apache.guacamole.protocol.GuacamoleStatus; +import org.apache.guacamole.GuacamoleException; /** - * An exception that will result in the given error error information being - * returned from the API layer. All error messages have the same format which - * is defined by APIError. + * An exception which exposes a given error within the API layer. When thrown + * within the context of the REST API, an appropriate HTTP status code will be + * set for the failing response, and the details of the error will be exposed in + * the body of the response as an APIError structure. * * @author James Muehlner * @author Michael Jumper @@ -37,88 +36,21 @@ import org.apache.guacamole.protocol.GuacamoleStatus; public class APIException extends WebApplicationException { /** - * Construct a new APIException with the given error. All information - * associated with this new exception will be extracted from the given - * APIError. + * Construct a new APIException based on the given GuacamoleException and + * HTTP status. The details of the GuacamoleException relevant to the REST + * API will be exposed via an APIError. * - * @param error - * The error that occurred. + * @param status + * The HTTP status which corresponds to the GuacamoleException. + * + * @param exception + * The GuacamoleException that occurred. */ - public APIException(APIError error) { - super(Response.status(error.getType().getStatus()) + public APIException(Response.Status status, GuacamoleException exception) { + super(Response.status(status) .type(MediaType.APPLICATION_JSON) - .entity(error) + .entity(new APIError(exception)) .build()); } - /** - * Creates a new APIException with the given type and message. The - * corresponding APIError will be created from the provided information. - * - * @param type - * The type of error that occurred. - * - * @param message - * A human-readable message describing the error. - */ - public APIException(APIError.Type type, String message) { - this(new APIError(type, message)); - } - - /** - * Creates a new APIException which represents an error that occurred within - * an intercepted Guacamole stream. The nature of that error will be - * described by a given status code, which should be the status code - * provided by the "ack" instruction that reported the error. - * - * @param status - * The Guacamole protocol status code describing the error that - * occurred within the intercepted stream. - * - * @param message - * An arbitrary human-readable message describing the error that - * occurred. - */ - public APIException(int status, String message) { - this(new APIError(status, message)); - } - - /** - * Creates a new APIException which represents an error that occurred within - * an intercepted Guacamole stream. The nature of that error will be - * described by a given Guacamole protocol status, which should be the - * status associated with the code provided by the "ack" instruction that - * reported the error. - * - * @param status - * The Guacamole protocol status describing the error that occurred - * within the intercepted stream. - * - * @param message - * An arbitrary human-readable message describing the error that - * occurred. - */ - public APIException(GuacamoleStatus status, String message) { - this(status.getGuacamoleStatusCode(), message); - } - - /** - * Creates a new APIException with the given type, message, and parameter - * information. The corresponding APIError will be created from the - * provided information. - * - * @param type - * The type of error that occurred. - * - * @param message - * A human-readable message describing the error. - * - * @param expected - * All parameters expected in the original request, or now required as - * a result of the original request, as a collection of fields. - */ - public APIException(APIError.Type type, String message, Collection expected) { - this(new APIError(type, message, expected)); - } - } diff --git a/guacamole/src/main/java/org/apache/guacamole/rest/RESTExceptionWrapper.java b/guacamole/src/main/java/org/apache/guacamole/rest/RESTExceptionWrapper.java index f4de4066b..8d6839f02 100644 --- a/guacamole/src/main/java/org/apache/guacamole/rest/RESTExceptionWrapper.java +++ b/guacamole/src/main/java/org/apache/guacamole/rest/RESTExceptionWrapper.java @@ -24,6 +24,8 @@ import java.lang.annotation.Annotation; import java.lang.reflect.Method; import javax.ws.rs.FormParam; import javax.ws.rs.QueryParam; +import javax.ws.rs.WebApplicationException; +import javax.ws.rs.core.Response; import org.aopalliance.intercept.MethodInterceptor; import org.aopalliance.intercept.MethodInvocation; import org.apache.guacamole.GuacamoleClientException; @@ -31,10 +33,7 @@ import org.apache.guacamole.GuacamoleException; import org.apache.guacamole.GuacamoleResourceNotFoundException; import org.apache.guacamole.GuacamoleSecurityException; import org.apache.guacamole.GuacamoleUnauthorizedException; -import org.apache.guacamole.net.auth.credentials.GuacamoleInsufficientCredentialsException; -import org.apache.guacamole.net.auth.credentials.GuacamoleInvalidCredentialsException; import org.apache.guacamole.rest.auth.AuthenticationService; -import org.apache.guacamole.tunnel.GuacamoleStreamException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -148,7 +147,7 @@ public class RESTExceptionWrapper implements MethodInterceptor { } @Override - public Object invoke(MethodInvocation invocation) throws Throwable { + public Object invoke(MethodInvocation invocation) throws WebApplicationException { try { @@ -172,108 +171,27 @@ public class RESTExceptionWrapper implements MethodInterceptor { } - // Rethrow unchecked exceptions such that they are properly wrapped - catch (RuntimeException e) { - throw new GuacamoleException(e.getMessage(), e); - } - } - // Additional credentials are needed - catch (GuacamoleInsufficientCredentialsException e) { - - // Generate default message - String message = e.getMessage(); - if (message == null) - message = "Permission denied."; - - throw new APIException( - APIError.Type.INSUFFICIENT_CREDENTIALS, - message, - e.getCredentialsInfo().getFields() - ); - } - - // The provided credentials are wrong - catch (GuacamoleInvalidCredentialsException e) { - - // Generate default message - String message = e.getMessage(); - if (message == null) - message = "Permission denied."; - - throw new APIException( - APIError.Type.INVALID_CREDENTIALS, - message, - e.getCredentialsInfo().getFields() - ); - } - - // Generic permission denied + // Translate GuacamoleException subclasses to HTTP error codes catch (GuacamoleSecurityException e) { - - // Generate default message - String message = e.getMessage(); - if (message == null) - message = "Permission denied."; - - throw new APIException( - APIError.Type.PERMISSION_DENIED, - message - ); - + throw new APIException(Response.Status.FORBIDDEN, e); } - - // Arbitrary resource not found catch (GuacamoleResourceNotFoundException e) { - - // Generate default message - String message = e.getMessage(); - if (message == null) - message = "Not found."; - - throw new APIException( - APIError.Type.NOT_FOUND, - message - ); - + throw new APIException(Response.Status.NOT_FOUND, e); } - - // Arbitrary bad requests catch (GuacamoleClientException e) { - - // Generate default message - String message = e.getMessage(); - if (message == null) - message = "Invalid request."; - - throw new APIException( - APIError.Type.BAD_REQUEST, - message - ); - + throw new APIException(Response.Status.BAD_REQUEST, e); } - - // Errors from intercepted streams - catch (GuacamoleStreamException e) { - - // Generate default message - String message = e.getMessage(); - if (message == null) - message = "Error reported by stream."; - - throw new APIException( - e.getStatus(), - message - ); - - } - - // All other errors catch (GuacamoleException e) { + throw new APIException(Response.Status.INTERNAL_SERVER_ERROR, e); + } - // Log all reasonable details of exception - String message = e.getMessage(); + // Rethrow unchecked exceptions such that they are properly wrapped + catch (Throwable t) { + + // Log all reasonable details of error + String message = t.getMessage(); if (message != null) logger.error("Unexpected internal error: {}", message); else @@ -282,12 +200,10 @@ public class RESTExceptionWrapper implements MethodInterceptor { + "details."); // Ensure internal errors are fully logged at the debug level - logger.debug("Unexpected exception in REST endpoint.", e); + logger.debug("Unexpected error in REST endpoint.", t); - throw new APIException( - APIError.Type.INTERNAL_ERROR, - "Unexpected server error." - ); + throw new APIException(Response.Status.INTERNAL_SERVER_ERROR, + new GuacamoleException("Unexpected internal error.", t)); } diff --git a/guacamole/src/main/java/org/apache/guacamole/rest/history/APIConnectionRecordSortPredicate.java b/guacamole/src/main/java/org/apache/guacamole/rest/history/APIConnectionRecordSortPredicate.java index b685087eb..a881f60eb 100644 --- a/guacamole/src/main/java/org/apache/guacamole/rest/history/APIConnectionRecordSortPredicate.java +++ b/guacamole/src/main/java/org/apache/guacamole/rest/history/APIConnectionRecordSortPredicate.java @@ -19,8 +19,9 @@ package org.apache.guacamole.rest.history; +import javax.ws.rs.core.Response; +import org.apache.guacamole.GuacamoleClientException; import org.apache.guacamole.net.auth.ConnectionRecordSet; -import org.apache.guacamole.rest.APIError; import org.apache.guacamole.rest.APIException; /** @@ -111,8 +112,8 @@ public class APIConnectionRecordSortPredicate { // Bail out if sort property is not valid catch (IllegalArgumentException e) { throw new APIException( - APIError.Type.BAD_REQUEST, - String.format("Invalid sort property: \"%s\"", value) + Response.Status.BAD_REQUEST, + new GuacamoleClientException(String.format("Invalid sort property: \"%s\"", value)) ); } From 0671f18d400a1adba977ec345baa966a0b88435d Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Tue, 16 Aug 2016 19:31:48 -0700 Subject: [PATCH 2/4] GUACAMOLE-36: Define REST API transfer mechanism for translatable messages. --- .../guacamole/language/Translatable.java | 41 ++++++++ .../language/TranslatableMessage.java | 96 +++++++++++++++++++ .../org/apache/guacamole/rest/APIError.java | 33 ++++++- .../src/main/webapp/app/rest/types/Error.js | 9 ++ .../app/rest/types/TranslatableMessage.js | 63 ++++++++++++ 5 files changed, 240 insertions(+), 2 deletions(-) create mode 100644 guacamole-ext/src/main/java/org/apache/guacamole/language/Translatable.java create mode 100644 guacamole-ext/src/main/java/org/apache/guacamole/language/TranslatableMessage.java create mode 100644 guacamole/src/main/webapp/app/rest/types/TranslatableMessage.js diff --git a/guacamole-ext/src/main/java/org/apache/guacamole/language/Translatable.java b/guacamole-ext/src/main/java/org/apache/guacamole/language/Translatable.java new file mode 100644 index 000000000..0b4381463 --- /dev/null +++ b/guacamole-ext/src/main/java/org/apache/guacamole/language/Translatable.java @@ -0,0 +1,41 @@ +/* + * 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.language; + +/** + * An interface which requires the definition of a translatable message that + * can be passed through an arbitrary translation service, producing a + * human-readable message in the user's native language. + * + * @author Michael Jumper + */ +public interface Translatable { + + /** + * Returns a message which can be translated using a translation service, + * consisting of a translation key and optional set of substitution + * variables. + * + * @return + * A message which can be translated using a translation service. + */ + TranslatableMessage getTranslatableMessage(); + +} diff --git a/guacamole-ext/src/main/java/org/apache/guacamole/language/TranslatableMessage.java b/guacamole-ext/src/main/java/org/apache/guacamole/language/TranslatableMessage.java new file mode 100644 index 000000000..4aeba9ab1 --- /dev/null +++ b/guacamole-ext/src/main/java/org/apache/guacamole/language/TranslatableMessage.java @@ -0,0 +1,96 @@ +/* + * 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.language; + +/** + * A message which can be translated using a translation service, providing a + * translation key and optional set of values to be substituted into the + * translation string associated with that key. + * + * @author Michael Jumper + */ +public class TranslatableMessage { + + /** + * The arbitrary key which can be used to look up the message to be + * displayed in the user's native language. + */ + private final String key; + + /** + * An arbitrary object whose properties should be substituted for the + * corresponding placeholders within the string associated with the key. + */ + private final Object variables; + + /** + * Creates a new TranslatableMessage associated with the given translation + * key, without any associated variables. + * + * @param key + * The translation key to associate with the TranslatableMessage. + */ + public TranslatableMessage(String key) { + this(key, null); + } + + /** + * Creates a new TranslatableMessage associated with the given translation + * key and associated variables. + * + * @param key + * The translation key to associate with the TranslatableMessage. + * + * @param variables + * An arbitrary object whose properties should be substituted for the + * corresponding placeholders within the string associated with the + * given translation key. + */ + public TranslatableMessage(String key, Object variables) { + this.key = key; + this.variables = variables; + } + + /** + * Returns the arbitrary key which can be used to look up the message to be + * displayed in the user's native language. + * + * @return + * The arbitrary key associated with the human-readable message. + */ + public String getKey() { + return key; + } + + /** + * Returns an arbitrary object whose properties should be substituted for + * the corresponding placeholders within the string associated with the key. + * If not applicable, null is returned. + * + * @return + * An arbitrary object whose properties should be substituted for the + * corresponding placeholders within the string associated with the key, + * or null if not applicable. + */ + public Object getVariables() { + return variables; + } + +} diff --git a/guacamole/src/main/java/org/apache/guacamole/rest/APIError.java b/guacamole/src/main/java/org/apache/guacamole/rest/APIError.java index 7727b85b0..2bec055ba 100644 --- a/guacamole/src/main/java/org/apache/guacamole/rest/APIError.java +++ b/guacamole/src/main/java/org/apache/guacamole/rest/APIError.java @@ -25,6 +25,8 @@ import org.apache.guacamole.GuacamoleException; import org.apache.guacamole.GuacamoleResourceNotFoundException; import org.apache.guacamole.GuacamoleSecurityException; import org.apache.guacamole.form.Field; +import org.apache.guacamole.language.Translatable; +import org.apache.guacamole.language.TranslatableMessage; import org.apache.guacamole.net.auth.credentials.GuacamoleCredentialsException; import org.apache.guacamole.net.auth.credentials.GuacamoleInsufficientCredentialsException; import org.apache.guacamole.net.auth.credentials.GuacamoleInvalidCredentialsException; @@ -39,10 +41,15 @@ import org.apache.guacamole.tunnel.GuacamoleStreamException; public class APIError { /** - * The error message. + * The human-readable error message. */ private final String message; + /** + * A translatable message representing the error that occurred. + */ + private final TranslatableMessage translatableMessage; + /** * The associated Guacamole protocol status code. */ @@ -148,7 +155,9 @@ public class APIError { /** * Creates a new APIError which exposes the details of the given - * GuacamoleException. + * GuacamoleException. If the given GuacamoleException implements + * Translatable, then its translation string and values will be exposed as + * well. * * @param exception * The GuacamoleException from which the details of the new APIError @@ -176,6 +185,14 @@ public class APIError { else this.statusCode = null; + // Pull translatable message and values if available + if (exception instanceof Translatable) { + Translatable translatable = (Translatable) exception; + this.translatableMessage = translatable.getTranslatableMessage(); + } + else + this.translatableMessage = new TranslatableMessage(this.message); + } /** @@ -223,4 +240,16 @@ public class APIError { return message; } + /** + * Returns a translatable message describing the error that occurred. If no + * translatable message is associated with the error, this will be null. + * + * @return + * A translatable message describing the error that occurred, or null + * if there is no such message defined. + */ + public TranslatableMessage getTranslatableMessage() { + return translatableMessage; + } + } diff --git a/guacamole/src/main/webapp/app/rest/types/Error.js b/guacamole/src/main/webapp/app/rest/types/Error.js index 43fdc9602..47f9cf770 100644 --- a/guacamole/src/main/webapp/app/rest/types/Error.js +++ b/guacamole/src/main/webapp/app/rest/types/Error.js @@ -42,6 +42,15 @@ angular.module('rest').factory('Error', [function defineError() { */ this.message = template.message; + /** + * A message which can be translated using the translation service, + * consisting of a translation key and optional set of substitution + * variables. + * + * @type TranslatableMessage + */ + this.translatableMessage = template.translatableMessage; + /** * The Guacamole protocol status code associated with the error that * occurred. This is only valid for errors of type STREAM_ERROR. diff --git a/guacamole/src/main/webapp/app/rest/types/TranslatableMessage.js b/guacamole/src/main/webapp/app/rest/types/TranslatableMessage.js new file mode 100644 index 000000000..454224840 --- /dev/null +++ b/guacamole/src/main/webapp/app/rest/types/TranslatableMessage.js @@ -0,0 +1,63 @@ +/* + * 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. + */ + +/** + * Service which defines the TranslatableMessage class. + */ +angular.module('rest').factory('TranslatableMessage', [function defineTranslatableMessage() { + + /** + * The object returned by REST API calls when representing a message which + * can be translated using the translation service, providing a translation + * key and optional set of values to be substituted into the translation + * string associated with that key. + * + * @constructor + * @param {TranslatableMessage|Object} [template={}] + * The object whose properties should be copied within the new + * TranslatableMessage. + */ + var TranslatableMessage = function TranslatableMessage(template) { + + // Use empty object by default + template = template || {}; + + /** + * The key associated with the translation string that used when + * displaying this message. + * + * @type String + */ + this.key = template.key; + + /** + * The object which should be passed through to the translation service + * for the sake of variable substitution. Each property of the provided + * object will be substituted for the variable of the same name within + * the translation string. + * + * @type Object + */ + this.variables = template.variables; + + }; + + return TranslatableMessage; + +}]); \ No newline at end of file From 986ea4b206ef6322cad629cecb4ec86dc7642d8a Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Tue, 16 Aug 2016 21:47:26 -0700 Subject: [PATCH 3/4] GUACAMOLE-36: Migrate guacLogin and guacNotification to TranslatableMessage. --- .../app/client/controllers/clientController.js | 16 ++++++++++++---- .../app/index/controllers/indexController.js | 4 ++-- .../main/webapp/app/login/directives/login.js | 10 ++++++---- .../main/webapp/app/login/templates/login.html | 6 ++++-- .../controllers/manageConnectionController.js | 8 +++++--- .../manageConnectionGroupController.js | 8 +++++--- .../manageSharingProfileController.js | 8 +++++--- .../manage/controllers/manageUserController.js | 15 ++++++++++----- .../notification/services/guacNotification.js | 4 +++- .../notification/templates/guacNotification.html | 4 +++- .../app/notification/types/Notification.js | 2 +- .../directives/guacSettingsPreferences.js | 14 ++++++++++---- .../settings/directives/guacSettingsSessions.js | 6 ++++-- 13 files changed, 70 insertions(+), 35 deletions(-) diff --git a/guacamole/src/main/webapp/app/client/controllers/clientController.js b/guacamole/src/main/webapp/app/client/controllers/clientController.js index b522568b7..9827de1e6 100644 --- a/guacamole/src/main/webapp/app/client/controllers/clientController.js +++ b/guacamole/src/main/webapp/app/client/controllers/clientController.js @@ -578,7 +578,9 @@ angular.module('client').controller('clientController', ['$scope', '$routeParams || connectionState === ManagedClientState.ConnectionState.WAITING) { guacNotification.showStatus({ title: "CLIENT.DIALOG_HEADER_CONNECTING", - text: "CLIENT.TEXT_CLIENT_STATUS_" + connectionState.toUpperCase() + text: { + key : "CLIENT.TEXT_CLIENT_STATUS_" + connectionState.toUpperCase() + } }); } @@ -595,7 +597,9 @@ angular.module('client').controller('clientController', ['$scope', '$routeParams notifyConnectionClosed({ className : "error", title : "CLIENT.DIALOG_HEADER_CONNECTION_ERROR", - text : "CLIENT.ERROR_CLIENT_" + errorName, + text : { + key : "CLIENT.ERROR_CLIENT_" + errorName + }, countdown : countdown, actions : actions }); @@ -615,7 +619,9 @@ angular.module('client').controller('clientController', ['$scope', '$routeParams notifyConnectionClosed({ className : "error", title : "CLIENT.DIALOG_HEADER_CONNECTION_ERROR", - text : "CLIENT.ERROR_TUNNEL_" + errorName, + text : { + key : "CLIENT.ERROR_TUNNEL_" + errorName + }, countdown : countdown, actions : actions }); @@ -626,7 +632,9 @@ angular.module('client').controller('clientController', ['$scope', '$routeParams else if (connectionState === ManagedClientState.ConnectionState.DISCONNECTED) { notifyConnectionClosed({ title : "CLIENT.DIALOG_HEADER_DISCONNECTED", - text : "CLIENT.TEXT_CLIENT_STATUS_" + connectionState.toUpperCase(), + text : { + key : "CLIENT.TEXT_CLIENT_STATUS_" + connectionState.toUpperCase() + }, actions : actions }); } diff --git a/guacamole/src/main/webapp/app/index/controllers/indexController.js b/guacamole/src/main/webapp/app/index/controllers/indexController.js index 25e6b6190..e17ecee1f 100644 --- a/guacamole/src/main/webapp/app/index/controllers/indexController.js +++ b/guacamole/src/main/webapp/app/index/controllers/indexController.js @@ -38,7 +38,7 @@ angular.module('index').controller('indexController', ['$scope', '$injector', * The message to display to the user as instructions for the login * process. * - * @type String + * @type TranslatableMessage */ $scope.loginHelpText = null; @@ -160,7 +160,7 @@ angular.module('index').controller('indexController', ['$scope', '$injector', $scope.$on('guacInsufficientCredentials', function loginInsufficient(event, parameters, error) { $scope.page.title = 'APP.NAME'; $scope.page.bodyClassName = ''; - $scope.loginHelpText = error.message; + $scope.loginHelpText = error.translatableMessage; $scope.acceptedCredentials = parameters; $scope.expectedCredentials = error.expected; }); diff --git a/guacamole/src/main/webapp/app/login/directives/login.js b/guacamole/src/main/webapp/app/login/directives/login.js index aa03af255..cc820b81e 100644 --- a/guacamole/src/main/webapp/app/login/directives/login.js +++ b/guacamole/src/main/webapp/app/login/directives/login.js @@ -36,7 +36,7 @@ angular.module('login').directive('guacLogin', [function guacLogin() { * An optional instructional message to display within the login * dialog. * - * @type String + * @type TranslatableMessage */ helpText : '=', @@ -72,7 +72,7 @@ angular.module('login').directive('guacLogin', [function guacLogin() { /** * A description of the error that occurred during login, if any. * - * @type String + * @type TranslatableMessage */ $scope.loginError = null; @@ -160,11 +160,13 @@ angular.module('login').directive('guacLogin', [function guacLogin() { // Flag generic error for invalid login if (error.type === Error.Type.INVALID_CREDENTIALS) - $scope.loginError = 'LOGIN.ERROR_INVALID_LOGIN'; + $scope.loginError = { + 'key' : 'LOGIN.ERROR_INVALID_LOGIN' + }; // Display error if anything else goes wrong else - $scope.loginError = error.message; + $scope.loginError = error.translatableMessage; // Clear all visible password fields angular.forEach($scope.remainingFields, function clearEnteredValueIfPassword(field) { diff --git a/guacamole/src/main/webapp/app/login/templates/login.html b/guacamole/src/main/webapp/app/login/templates/login.html index b9d9335f4..26a3f1889 100644 --- a/guacamole/src/main/webapp/app/login/templates/login.html +++ b/guacamole/src/main/webapp/app/login/templates/login.html @@ -1,7 +1,8 @@