GUACAMOLE-36: Merge new system for fully translatable error messages.

This commit is contained in:
James Muehlner
2017-01-03 19:57:42 -08:00
21 changed files with 421 additions and 301 deletions

View File

@@ -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 object which is associated with 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();
}

View File

@@ -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;
}
}

View File

@@ -20,8 +20,17 @@
package org.apache.guacamole.rest; package org.apache.guacamole.rest;
import java.util.Collection; 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.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;
import org.apache.guacamole.tunnel.GuacamoleStreamException;
/** /**
* Describes an error that occurred within a REST endpoint. * Describes an error that occurred within a REST endpoint.
@@ -32,10 +41,15 @@ import org.apache.guacamole.form.Field;
public class APIError { public class APIError {
/** /**
* The error message. * The human-readable error message.
*/ */
private final String message; private final String message;
/**
* A translatable message representing the error that occurred.
*/
private final TranslatableMessage translatableMessage;
/** /**
* The associated Guacamole protocol status code. * The associated Guacamole protocol status code.
*/ */
@@ -60,124 +74,125 @@ public class APIError {
* The requested operation could not be performed because the request * The requested operation could not be performed because the request
* itself was malformed. * itself was malformed.
*/ */
BAD_REQUEST(Response.Status.BAD_REQUEST), BAD_REQUEST,
/** /**
* The credentials provided were invalid. * The credentials provided were invalid.
*/ */
INVALID_CREDENTIALS(Response.Status.FORBIDDEN), INVALID_CREDENTIALS,
/** /**
* The credentials provided were not necessarily invalid, but were not * The credentials provided were not necessarily invalid, but were not
* sufficient to determine validity. * sufficient to determine validity.
*/ */
INSUFFICIENT_CREDENTIALS(Response.Status.FORBIDDEN), INSUFFICIENT_CREDENTIALS,
/** /**
* An internal server error has occurred. * An internal server error has occurred.
*/ */
INTERNAL_ERROR(Response.Status.INTERNAL_SERVER_ERROR), INTERNAL_ERROR,
/** /**
* An object related to the request does not exist. * 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 was denied to perform the requested operation.
*/ */
PERMISSION_DENIED(Response.Status.FORBIDDEN), PERMISSION_DENIED,
/** /**
* An error occurred within an intercepted stream, terminating that * An error occurred within an intercepted stream, terminating that
* stream. The Guacamole protocol status code of that error can be * stream. The Guacamole protocol status code of that error can be
* retrieved with getStatusCode(). * retrieved with getStatusCode().
*/ */
STREAM_ERROR(Response.Status.BAD_REQUEST); STREAM_ERROR;
/** /**
* The HTTP status associated with this error type. * Returns the REST API error type which corresponds to the type of the
*/ * given exception.
private final Response.Status status;
/**
* Defines a new error type associated with the given HTTP status.
* *
* @param status * @param exception
* The HTTP status to associate with the error type. * The exception to use to derive the API error type.
*/
Type(Response.Status status) {
this.status = status;
}
/**
* Returns the HTTP status associated with this error type.
* *
* @return * @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() { public static Type fromGuacamoleException(GuacamoleException exception) {
return status;
// 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 * Creates a new APIError which exposes the details of the given
* Guacamole protocol status code and human-readable message. The status * GuacamoleException. If the given GuacamoleException implements
* code and message should be taken directly from the "ack" instruction * Translatable, then its translation string and values will be exposed as
* causing the error. * well.
* *
* @param statusCode * @param exception
* The Guacamole protocol status code describing the error that * The GuacamoleException from which the details of the new APIError
* occurred within the intercepted stream. * should be derived.
*
* @param message
* An arbitrary human-readable message describing the error that
* occurred.
*/ */
public APIError(int statusCode, String message) { public APIError(GuacamoleException exception) {
this.type = Type.STREAM_ERROR;
this.message = message; // Build base REST service error
this.statusCode = statusCode; 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; this.expected = null;
}
/** // Add stream status code if applicable
* Create a new APIError with the specified error message. if (exception instanceof GuacamoleStreamException) {
* GuacamoleStreamException streamException = (GuacamoleStreamException) exception;
* @param type this.statusCode = streamException.getStatus().getGuacamoleStatusCode();
* 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;
} }
else
/**
* 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<Field> expected) {
this.type = type;
this.message = message;
this.statusCode = null; this.statusCode = null;
this.expected = expected;
// 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);
} }
/** /**
@@ -225,4 +240,16 @@ public class APIError {
return message; 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;
}
} }

View File

@@ -19,17 +19,16 @@
package org.apache.guacamole.rest; package org.apache.guacamole.rest;
import java.util.Collection;
import javax.ws.rs.WebApplicationException; import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.MediaType; import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response; import javax.ws.rs.core.Response;
import org.apache.guacamole.form.Field; import org.apache.guacamole.GuacamoleException;
import org.apache.guacamole.protocol.GuacamoleStatus;
/** /**
* An exception that will result in the given error error information being * An exception which exposes a given error within the API layer. When thrown
* returned from the API layer. All error messages have the same format which * within the context of the REST API, an appropriate HTTP status code will be
* is defined by APIError. * 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 James Muehlner
* @author Michael Jumper * @author Michael Jumper
@@ -37,88 +36,21 @@ import org.apache.guacamole.protocol.GuacamoleStatus;
public class APIException extends WebApplicationException { public class APIException extends WebApplicationException {
/** /**
* Construct a new APIException with the given error. All information * Construct a new APIException based on the given GuacamoleException and
* associated with this new exception will be extracted from the given * HTTP status. The details of the GuacamoleException relevant to the REST
* APIError. * API will be exposed via an APIError.
* *
* @param error * @param status
* The error that occurred. * The HTTP status which corresponds to the GuacamoleException.
*
* @param exception
* The GuacamoleException that occurred.
*/ */
public APIException(APIError error) { public APIException(Response.Status status, GuacamoleException exception) {
super(Response.status(error.getType().getStatus()) super(Response.status(status)
.type(MediaType.APPLICATION_JSON) .type(MediaType.APPLICATION_JSON)
.entity(error) .entity(new APIError(exception))
.build()); .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<Field> expected) {
this(new APIError(type, message, expected));
}
} }

View File

@@ -24,6 +24,8 @@ import java.lang.annotation.Annotation;
import java.lang.reflect.Method; import java.lang.reflect.Method;
import javax.ws.rs.FormParam; import javax.ws.rs.FormParam;
import javax.ws.rs.QueryParam; 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.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation; import org.aopalliance.intercept.MethodInvocation;
import org.apache.guacamole.GuacamoleClientException; import org.apache.guacamole.GuacamoleClientException;
@@ -31,10 +33,7 @@ import org.apache.guacamole.GuacamoleException;
import org.apache.guacamole.GuacamoleResourceNotFoundException; import org.apache.guacamole.GuacamoleResourceNotFoundException;
import org.apache.guacamole.GuacamoleSecurityException; import org.apache.guacamole.GuacamoleSecurityException;
import org.apache.guacamole.GuacamoleUnauthorizedException; 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.rest.auth.AuthenticationService;
import org.apache.guacamole.tunnel.GuacamoleStreamException;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@@ -148,7 +147,7 @@ public class RESTExceptionWrapper implements MethodInterceptor {
} }
@Override @Override
public Object invoke(MethodInvocation invocation) throws Throwable { public Object invoke(MethodInvocation invocation) throws WebApplicationException {
try { 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);
} }
} // Translate GuacamoleException subclasses to HTTP error codes
// 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
catch (GuacamoleSecurityException e) { catch (GuacamoleSecurityException e) {
throw new APIException(Response.Status.FORBIDDEN, e);
// Generate default message
String message = e.getMessage();
if (message == null)
message = "Permission denied.";
throw new APIException(
APIError.Type.PERMISSION_DENIED,
message
);
} }
// Arbitrary resource not found
catch (GuacamoleResourceNotFoundException e) { catch (GuacamoleResourceNotFoundException e) {
throw new APIException(Response.Status.NOT_FOUND, e);
// Generate default message
String message = e.getMessage();
if (message == null)
message = "Not found.";
throw new APIException(
APIError.Type.NOT_FOUND,
message
);
} }
// Arbitrary bad requests
catch (GuacamoleClientException e) { catch (GuacamoleClientException e) {
throw new APIException(Response.Status.BAD_REQUEST, e);
// Generate default message
String message = e.getMessage();
if (message == null)
message = "Invalid request.";
throw new APIException(
APIError.Type.BAD_REQUEST,
message
);
} }
// 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) { catch (GuacamoleException e) {
throw new APIException(Response.Status.INTERNAL_SERVER_ERROR, e);
}
// Log all reasonable details of exception // Rethrow unchecked exceptions such that they are properly wrapped
String message = e.getMessage(); catch (Throwable t) {
// Log all reasonable details of error
String message = t.getMessage();
if (message != null) if (message != null)
logger.error("Unexpected internal error: {}", message); logger.error("Unexpected internal error: {}", message);
else else
@@ -282,12 +200,10 @@ public class RESTExceptionWrapper implements MethodInterceptor {
+ "details."); + "details.");
// Ensure internal errors are fully logged at the debug level // 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( throw new APIException(Response.Status.INTERNAL_SERVER_ERROR,
APIError.Type.INTERNAL_ERROR, new GuacamoleException("Unexpected internal error.", t));
"Unexpected server error."
);
} }

View File

@@ -19,8 +19,9 @@
package org.apache.guacamole.rest.history; 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.net.auth.ConnectionRecordSet;
import org.apache.guacamole.rest.APIError;
import org.apache.guacamole.rest.APIException; import org.apache.guacamole.rest.APIException;
/** /**
@@ -111,8 +112,8 @@ public class APIConnectionRecordSortPredicate {
// Bail out if sort property is not valid // Bail out if sort property is not valid
catch (IllegalArgumentException e) { catch (IllegalArgumentException e) {
throw new APIException( throw new APIException(
APIError.Type.BAD_REQUEST, Response.Status.BAD_REQUEST,
String.format("Invalid sort property: \"%s\"", value) new GuacamoleClientException(String.format("Invalid sort property: \"%s\"", value))
); );
} }

View File

@@ -578,7 +578,9 @@ angular.module('client').controller('clientController', ['$scope', '$routeParams
|| connectionState === ManagedClientState.ConnectionState.WAITING) { || connectionState === ManagedClientState.ConnectionState.WAITING) {
guacNotification.showStatus({ guacNotification.showStatus({
title: "CLIENT.DIALOG_HEADER_CONNECTING", 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({ notifyConnectionClosed({
className : "error", className : "error",
title : "CLIENT.DIALOG_HEADER_CONNECTION_ERROR", title : "CLIENT.DIALOG_HEADER_CONNECTION_ERROR",
text : "CLIENT.ERROR_CLIENT_" + errorName, text : {
key : "CLIENT.ERROR_CLIENT_" + errorName
},
countdown : countdown, countdown : countdown,
actions : actions actions : actions
}); });
@@ -615,7 +619,9 @@ angular.module('client').controller('clientController', ['$scope', '$routeParams
notifyConnectionClosed({ notifyConnectionClosed({
className : "error", className : "error",
title : "CLIENT.DIALOG_HEADER_CONNECTION_ERROR", title : "CLIENT.DIALOG_HEADER_CONNECTION_ERROR",
text : "CLIENT.ERROR_TUNNEL_" + errorName, text : {
key : "CLIENT.ERROR_TUNNEL_" + errorName
},
countdown : countdown, countdown : countdown,
actions : actions actions : actions
}); });
@@ -626,7 +632,9 @@ angular.module('client').controller('clientController', ['$scope', '$routeParams
else if (connectionState === ManagedClientState.ConnectionState.DISCONNECTED) { else if (connectionState === ManagedClientState.ConnectionState.DISCONNECTED) {
notifyConnectionClosed({ notifyConnectionClosed({
title : "CLIENT.DIALOG_HEADER_DISCONNECTED", title : "CLIENT.DIALOG_HEADER_DISCONNECTED",
text : "CLIENT.TEXT_CLIENT_STATUS_" + connectionState.toUpperCase(), text : {
key : "CLIENT.TEXT_CLIENT_STATUS_" + connectionState.toUpperCase()
},
actions : actions actions : actions
}); });
} }

View File

@@ -38,7 +38,7 @@ angular.module('index').controller('indexController', ['$scope', '$injector',
* The message to display to the user as instructions for the login * The message to display to the user as instructions for the login
* process. * process.
* *
* @type String * @type TranslatableMessage
*/ */
$scope.loginHelpText = null; $scope.loginHelpText = null;
@@ -160,7 +160,7 @@ angular.module('index').controller('indexController', ['$scope', '$injector',
$scope.$on('guacInsufficientCredentials', function loginInsufficient(event, parameters, error) { $scope.$on('guacInsufficientCredentials', function loginInsufficient(event, parameters, error) {
$scope.page.title = 'APP.NAME'; $scope.page.title = 'APP.NAME';
$scope.page.bodyClassName = ''; $scope.page.bodyClassName = '';
$scope.loginHelpText = error.message; $scope.loginHelpText = error.translatableMessage;
$scope.acceptedCredentials = parameters; $scope.acceptedCredentials = parameters;
$scope.expectedCredentials = error.expected; $scope.expectedCredentials = error.expected;
}); });

View File

@@ -36,7 +36,7 @@ angular.module('login').directive('guacLogin', [function guacLogin() {
* An optional instructional message to display within the login * An optional instructional message to display within the login
* dialog. * dialog.
* *
* @type String * @type TranslatableMessage
*/ */
helpText : '=', helpText : '=',
@@ -72,7 +72,7 @@ angular.module('login').directive('guacLogin', [function guacLogin() {
/** /**
* A description of the error that occurred during login, if any. * A description of the error that occurred during login, if any.
* *
* @type String * @type TranslatableMessage
*/ */
$scope.loginError = null; $scope.loginError = null;
@@ -160,11 +160,13 @@ angular.module('login').directive('guacLogin', [function guacLogin() {
// Flag generic error for invalid login // Flag generic error for invalid login
if (error.type === Error.Type.INVALID_CREDENTIALS) 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 // Display error if anything else goes wrong
else else
$scope.loginError = error.message; $scope.loginError = error.translatableMessage;
// Clear all visible password fields // Clear all visible password fields
angular.forEach($scope.remainingFields, function clearEnteredValueIfPassword(field) { angular.forEach($scope.remainingFields, function clearEnteredValueIfPassword(field) {

View File

@@ -1,7 +1,8 @@
<div class="login-ui" ng-class="{error: loginError, continuation: isContinuation(), initial: !isContinuation()}" > <div class="login-ui" ng-class="{error: loginError, continuation: isContinuation(), initial: !isContinuation()}" >
<!-- Login error message --> <!-- Login error message -->
<p class="login-error">{{loginError | translate}}</p> <p class="login-error" translate="{{loginError.key}}"
translate-values="{{loginError.variables}}"></p>
<div class="login-dialog-middle"> <div class="login-dialog-middle">
@@ -17,7 +18,8 @@
</div> </div>
<!-- Login message/instructions --> <!-- Login message/instructions -->
<p ng-show="helpText">{{helpText | translate}}</p> <p ng-show="helpText" translate="{{helpText.key}}"
translate-values="{{helpText.variables}}"></p>
<!-- Login fields --> <!-- Login fields -->
<div class="login-fields"> <div class="login-fields">

View File

@@ -380,7 +380,7 @@ angular.module('manage').controller('manageConnectionController', ['$scope', '$i
guacNotification.showStatus({ guacNotification.showStatus({
'className' : 'error', 'className' : 'error',
'title' : 'MANAGE_CONNECTION.DIALOG_HEADER_ERROR', 'title' : 'MANAGE_CONNECTION.DIALOG_HEADER_ERROR',
'text' : error.message, 'text' : error.translatableMessage,
'actions' : [ ACKNOWLEDGE_ACTION ] 'actions' : [ ACKNOWLEDGE_ACTION ]
}); });
}); });
@@ -430,7 +430,7 @@ angular.module('manage').controller('manageConnectionController', ['$scope', '$i
guacNotification.showStatus({ guacNotification.showStatus({
'className' : 'error', 'className' : 'error',
'title' : 'MANAGE_CONNECTION.DIALOG_HEADER_ERROR', 'title' : 'MANAGE_CONNECTION.DIALOG_HEADER_ERROR',
'text' : error.message, 'text' : error.translatableMessage,
'actions' : [ ACKNOWLEDGE_ACTION ] 'actions' : [ ACKNOWLEDGE_ACTION ]
}); });
}); });
@@ -446,7 +446,9 @@ angular.module('manage').controller('manageConnectionController', ['$scope', '$i
// Confirm deletion request // Confirm deletion request
guacNotification.showStatus({ guacNotification.showStatus({
'title' : 'MANAGE_CONNECTION.DIALOG_HEADER_CONFIRM_DELETE', 'title' : 'MANAGE_CONNECTION.DIALOG_HEADER_CONFIRM_DELETE',
'text' : 'MANAGE_CONNECTION.TEXT_CONFIRM_DELETE', 'text' : {
key : 'MANAGE_CONNECTION.TEXT_CONFIRM_DELETE'
},
'actions' : [ DELETE_ACTION, CANCEL_ACTION] 'actions' : [ DELETE_ACTION, CANCEL_ACTION]
}); });

View File

@@ -220,7 +220,7 @@ angular.module('manage').controller('manageConnectionGroupController', ['$scope'
guacNotification.showStatus({ guacNotification.showStatus({
'className' : 'error', 'className' : 'error',
'title' : 'MANAGE_CONNECTION_GROUP.DIALOG_HEADER_ERROR', 'title' : 'MANAGE_CONNECTION_GROUP.DIALOG_HEADER_ERROR',
'text' : error.message, 'text' : error.translatableMessage,
'actions' : [ ACKNOWLEDGE_ACTION ] 'actions' : [ ACKNOWLEDGE_ACTION ]
}); });
}); });
@@ -270,7 +270,7 @@ angular.module('manage').controller('manageConnectionGroupController', ['$scope'
guacNotification.showStatus({ guacNotification.showStatus({
'className' : 'error', 'className' : 'error',
'title' : 'MANAGE_CONNECTION_GROUP.DIALOG_HEADER_ERROR', 'title' : 'MANAGE_CONNECTION_GROUP.DIALOG_HEADER_ERROR',
'text' : error.message, 'text' : error.translatableMessage,
'actions' : [ ACKNOWLEDGE_ACTION ] 'actions' : [ ACKNOWLEDGE_ACTION ]
}); });
}); });
@@ -286,7 +286,9 @@ angular.module('manage').controller('manageConnectionGroupController', ['$scope'
// Confirm deletion request // Confirm deletion request
guacNotification.showStatus({ guacNotification.showStatus({
'title' : 'MANAGE_CONNECTION_GROUP.DIALOG_HEADER_CONFIRM_DELETE', 'title' : 'MANAGE_CONNECTION_GROUP.DIALOG_HEADER_CONFIRM_DELETE',
'text' : 'MANAGE_CONNECTION_GROUP.TEXT_CONFIRM_DELETE', 'text' : {
key : 'MANAGE_CONNECTION_GROUP.TEXT_CONFIRM_DELETE'
},
'actions' : [ DELETE_ACTION, CANCEL_ACTION] 'actions' : [ DELETE_ACTION, CANCEL_ACTION]
}); });

View File

@@ -341,7 +341,7 @@ angular.module('manage').controller('manageSharingProfileController', ['$scope',
guacNotification.showStatus({ guacNotification.showStatus({
'className' : 'error', 'className' : 'error',
'title' : 'MANAGE_SHARING_PROFILE.DIALOG_HEADER_ERROR', 'title' : 'MANAGE_SHARING_PROFILE.DIALOG_HEADER_ERROR',
'text' : error.message, 'text' : error.translatableMessage,
'actions' : [ ACKNOWLEDGE_ACTION ] 'actions' : [ ACKNOWLEDGE_ACTION ]
}); });
}); });
@@ -379,7 +379,7 @@ angular.module('manage').controller('manageSharingProfileController', ['$scope',
guacNotification.showStatus({ guacNotification.showStatus({
'className' : 'error', 'className' : 'error',
'title' : 'MANAGE_SHARING_PROFILE.DIALOG_HEADER_ERROR', 'title' : 'MANAGE_SHARING_PROFILE.DIALOG_HEADER_ERROR',
'text' : error.message, 'text' : error.translatableMessage,
'actions' : [ ACKNOWLEDGE_ACTION ] 'actions' : [ ACKNOWLEDGE_ACTION ]
}); });
}); });
@@ -395,7 +395,9 @@ angular.module('manage').controller('manageSharingProfileController', ['$scope',
// Confirm deletion request // Confirm deletion request
guacNotification.showStatus({ guacNotification.showStatus({
'title' : 'MANAGE_SHARING_PROFILE.DIALOG_HEADER_CONFIRM_DELETE', 'title' : 'MANAGE_SHARING_PROFILE.DIALOG_HEADER_CONFIRM_DELETE',
'text' : 'MANAGE_SHARING_PROFILE.TEXT_CONFIRM_DELETE', 'text' : {
'key' : 'MANAGE_SHARING_PROFILE.TEXT_CONFIRM_DELETE'
},
'actions' : [ DELETE_ACTION, CANCEL_ACTION] 'actions' : [ DELETE_ACTION, CANCEL_ACTION]
}); });

View File

@@ -1012,7 +1012,9 @@ angular.module('manage').controller('manageUserController', ['$scope', '$injecto
guacNotification.showStatus({ guacNotification.showStatus({
'className' : 'error', 'className' : 'error',
'title' : 'MANAGE_USER.DIALOG_HEADER_ERROR', 'title' : 'MANAGE_USER.DIALOG_HEADER_ERROR',
'text' : 'MANAGE_USER.ERROR_PASSWORD_MISMATCH', 'text' : {
key : 'MANAGE_USER.ERROR_PASSWORD_MISMATCH'
},
'actions' : [ ACKNOWLEDGE_ACTION ] 'actions' : [ ACKNOWLEDGE_ACTION ]
}); });
return; return;
@@ -1055,7 +1057,8 @@ angular.module('manage').controller('manageUserController', ['$scope', '$injecto
guacNotification.showStatus({ guacNotification.showStatus({
'className' : 'error', 'className' : 'error',
'title' : 'MANAGE_USER.DIALOG_HEADER_ERROR', 'title' : 'MANAGE_USER.DIALOG_HEADER_ERROR',
'text' : error.message, 'text' : error.translatableMessage,
'values' : error.translationValues,
'actions' : [ ACKNOWLEDGE_ACTION ] 'actions' : [ ACKNOWLEDGE_ACTION ]
}); });
}); });
@@ -1067,7 +1070,7 @@ angular.module('manage').controller('manageUserController', ['$scope', '$injecto
guacNotification.showStatus({ guacNotification.showStatus({
'className' : 'error', 'className' : 'error',
'title' : 'MANAGE_USER.DIALOG_HEADER_ERROR', 'title' : 'MANAGE_USER.DIALOG_HEADER_ERROR',
'text' : error.message, 'text' : error.translatableMessage,
'actions' : [ ACKNOWLEDGE_ACTION ] 'actions' : [ ACKNOWLEDGE_ACTION ]
}); });
}); });
@@ -1117,7 +1120,7 @@ angular.module('manage').controller('manageUserController', ['$scope', '$injecto
guacNotification.showStatus({ guacNotification.showStatus({
'className' : 'error', 'className' : 'error',
'title' : 'MANAGE_USER.DIALOG_HEADER_ERROR', 'title' : 'MANAGE_USER.DIALOG_HEADER_ERROR',
'text' : error.message, 'text' : error.translatableMessage,
'actions' : [ ACKNOWLEDGE_ACTION ] 'actions' : [ ACKNOWLEDGE_ACTION ]
}); });
}); });
@@ -1133,7 +1136,9 @@ angular.module('manage').controller('manageUserController', ['$scope', '$injecto
// Confirm deletion request // Confirm deletion request
guacNotification.showStatus({ guacNotification.showStatus({
'title' : 'MANAGE_USER.DIALOG_HEADER_CONFIRM_DELETE', 'title' : 'MANAGE_USER.DIALOG_HEADER_CONFIRM_DELETE',
'text' : 'MANAGE_USER.TEXT_CONFIRM_DELETE', 'text' : {
key : 'MANAGE_USER.TEXT_CONFIRM_DELETE'
},
'actions' : [ DELETE_ACTION, CANCEL_ACTION] 'actions' : [ DELETE_ACTION, CANCEL_ACTION]
}); });

View File

@@ -60,7 +60,9 @@ angular.module('notification').factory('guacNotification', ['$injector',
* // To show a status message with actions * // To show a status message with actions
* guacNotification.showStatus({ * guacNotification.showStatus({
* 'title' : 'Disconnected', * 'title' : 'Disconnected',
* 'text' : 'You have been disconnected!', * 'text' : {
* 'key' : 'NAMESPACE.SOME_TRANSLATION_KEY'
* },
* 'actions' : { * 'actions' : {
* 'name' : 'reconnect', * 'name' : 'reconnect',
* 'callback' : function () { * 'callback' : function () {

View File

@@ -8,7 +8,9 @@
<div class="body"> <div class="body">
<!-- Notification text --> <!-- Notification text -->
<p ng-show="notification.text" class="text">{{notification.text | translate}}</p> <p ng-show="notification.text" class="text"
translate="{{notification.text.key}}"
translate-values="{{notification.text.variables}}"></p>
<!-- Current progress --> <!-- Current progress -->
<div class="progress" ng-show="notification.progress"><div class="bar" ng-show="progressPercent" ng-style="{'width': progressPercent + '%'}"></div><div <div class="progress" ng-show="notification.progress"><div class="bar" ng-show="progressPercent" ng-style="{'width': progressPercent + '%'}"></div><div

View File

@@ -53,7 +53,7 @@ angular.module('notification').factory('Notification', [function defineNotificat
/** /**
* The body text of the notification. * The body text of the notification.
* *
* @type String * @type TranslatableMessage
*/ */
this.text = template.text; this.text = template.text;

View File

@@ -42,6 +42,15 @@ angular.module('rest').factory('Error', [function defineError() {
*/ */
this.message = template.message; 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 * The Guacamole protocol status code associated with the error that
* occurred. This is only valid for errors of type STREAM_ERROR. * occurred. This is only valid for errors of type STREAM_ERROR.

View File

@@ -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;
}]);

View File

@@ -127,7 +127,9 @@ angular.module('settings').directive('guacSettingsPreferences', [function guacSe
guacNotification.showStatus({ guacNotification.showStatus({
className : 'error', className : 'error',
title : 'SETTINGS_PREFERENCES.DIALOG_HEADER_ERROR', title : 'SETTINGS_PREFERENCES.DIALOG_HEADER_ERROR',
text : 'SETTINGS_PREFERENCES.ERROR_PASSWORD_MISMATCH', text : {
key : 'SETTINGS_PREFERENCES.ERROR_PASSWORD_MISMATCH'
},
actions : [ ACKNOWLEDGE_ACTION ] actions : [ ACKNOWLEDGE_ACTION ]
}); });
return; return;
@@ -138,7 +140,9 @@ angular.module('settings').directive('guacSettingsPreferences', [function guacSe
guacNotification.showStatus({ guacNotification.showStatus({
className : 'error', className : 'error',
title : 'SETTINGS_PREFERENCES.DIALOG_HEADER_ERROR', title : 'SETTINGS_PREFERENCES.DIALOG_HEADER_ERROR',
text : 'SETTINGS_PREFERENCES.ERROR_PASSWORD_BLANK', text : {
key : 'SETTINGS_PREFERENCES.ERROR_PASSWORD_BLANK'
},
actions : [ ACKNOWLEDGE_ACTION ] actions : [ ACKNOWLEDGE_ACTION ]
}); });
return; return;
@@ -155,7 +159,9 @@ angular.module('settings').directive('guacSettingsPreferences', [function guacSe
// Indicate that the password has been changed // Indicate that the password has been changed
guacNotification.showStatus({ guacNotification.showStatus({
text : 'SETTINGS_PREFERENCES.INFO_PASSWORD_CHANGED', text : {
key : 'SETTINGS_PREFERENCES.INFO_PASSWORD_CHANGED'
},
actions : [ ACKNOWLEDGE_ACTION ] actions : [ ACKNOWLEDGE_ACTION ]
}); });
}) })
@@ -165,7 +171,7 @@ angular.module('settings').directive('guacSettingsPreferences', [function guacSe
guacNotification.showStatus({ guacNotification.showStatus({
className : 'error', className : 'error',
title : 'SETTINGS_PREFERENCES.DIALOG_HEADER_ERROR', title : 'SETTINGS_PREFERENCES.DIALOG_HEADER_ERROR',
'text' : error.message, text : error.translatableMessage,
actions : [ ACKNOWLEDGE_ACTION ] actions : [ ACKNOWLEDGE_ACTION ]
}); });
}); });

View File

@@ -334,7 +334,7 @@ angular.module('settings').directive('guacSettingsSessions', [function guacSetti
guacNotification.showStatus({ guacNotification.showStatus({
'className' : 'error', 'className' : 'error',
'title' : 'SETTINGS_SESSIONS.DIALOG_HEADER_ERROR', 'title' : 'SETTINGS_SESSIONS.DIALOG_HEADER_ERROR',
'text' : error.message, 'text' : error.translatableMessage,
'actions' : [ ACKNOWLEDGE_ACTION ] 'actions' : [ ACKNOWLEDGE_ACTION ]
}); });
}); });
@@ -349,7 +349,9 @@ angular.module('settings').directive('guacSettingsSessions', [function guacSetti
// Confirm deletion request // Confirm deletion request
guacNotification.showStatus({ guacNotification.showStatus({
'title' : 'SETTINGS_SESSIONS.DIALOG_HEADER_CONFIRM_DELETE', 'title' : 'SETTINGS_SESSIONS.DIALOG_HEADER_CONFIRM_DELETE',
'text' : 'SETTINGS_SESSIONS.TEXT_CONFIRM_DELETE', 'text' : {
'key' : 'SETTINGS_SESSIONS.TEXT_CONFIRM_DELETE'
},
'actions' : [ DELETE_ACTION, CANCEL_ACTION] 'actions' : [ DELETE_ACTION, CANCEL_ACTION]
}); });
}; };