From ac6e92860b2aedbc156d31e1c3317b9f03690b9f Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Tue, 16 Dec 2014 17:47:15 -0800 Subject: [PATCH] GUAC-932: Add support for recursive query (and break JS). --- .../guacamole/net/basic/rest/RESTModule.java | 2 - .../connectiongroup/APIConnectionGroup.java | 62 ++++++ .../ConnectionGroupRESTService.java | 189 ++++++++++++------ .../ConnectionGroupService.java | 59 ------ .../rest/services/connectionGroupService.js | 31 +-- .../webapp/app/rest/types/ConnectionGroup.js | 18 ++ 6 files changed, 227 insertions(+), 134 deletions(-) delete mode 100644 guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/connectiongroup/ConnectionGroupService.java diff --git a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/RESTModule.java b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/RESTModule.java index 48ac52b75..1c2a9d6c5 100644 --- a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/RESTModule.java +++ b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/RESTModule.java @@ -24,7 +24,6 @@ package org.glyptodon.guacamole.net.basic.rest; import com.google.inject.AbstractModule; import org.glyptodon.guacamole.net.basic.rest.connection.ConnectionService; -import org.glyptodon.guacamole.net.basic.rest.connectiongroup.ConnectionGroupService; import org.glyptodon.guacamole.net.basic.rest.protocol.ProtocolRetrievalService; /** @@ -40,7 +39,6 @@ public class RESTModule extends AbstractModule { // Bind generic low-level services bind(ConnectionService.class); - bind(ConnectionGroupService.class); bind(ProtocolRetrievalService.class); } diff --git a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/connectiongroup/APIConnectionGroup.java b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/connectiongroup/APIConnectionGroup.java index 16d2a0121..3f166f41c 100644 --- a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/connectiongroup/APIConnectionGroup.java +++ b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/connectiongroup/APIConnectionGroup.java @@ -22,9 +22,11 @@ package org.glyptodon.guacamole.net.basic.rest.connectiongroup; +import java.util.Collection; import org.codehaus.jackson.annotate.JsonIgnoreProperties; import org.glyptodon.guacamole.net.auth.ConnectionGroup; import org.glyptodon.guacamole.net.auth.ConnectionGroup.Type; +import org.glyptodon.guacamole.net.basic.rest.connection.APIConnection; /** * A simple connection group to expose through the REST endpoints. @@ -58,6 +60,18 @@ public class APIConnectionGroup { * The type of this connection group. */ private Type type; + + /** + * All child connection groups. If children are not being queried, this may + * be omitted. + */ + private Collection childConnectionGroups; + + /** + * All child connections. If children are not being queried, this may be + * omitted. + */ + private Collection childConnections; /** * Create an empty APIConnectionGroup. @@ -148,4 +162,52 @@ public class APIConnectionGroup { this.type = type; } + /** + * Returns a collection of all child connection groups, or null if children + * have not been queried. + * + * @return + * A collection of all child connection groups, or null if children + * have not been queried. + */ + public Collection getChildConnectionGroups() { + return childConnectionGroups; + } + + /** + * Sets the collection of all child connection groups to the given + * collection, which may be null if children have not been queried. + * + * @param childConnectionGroups + * The collection containing all child connection groups of this + * connection group, or null if children have not been queried. + */ + public void setChildConnectionGroups(Collection childConnectionGroups) { + this.childConnectionGroups = childConnectionGroups; + } + + /** + * Returns a collection of all child connections, or null if children have + * not been queried. + * + * @return + * A collection of all child connections, or null if children have not + * been queried. + */ + public Collection getChildConnections() { + return childConnections; + } + + /** + * Sets the collection of all child connections to the given collection, + * which may be null if children have not been queried. + * + * @param childConnections + * The collection containing all child connections of this connection + * group, or null if children have not been queried. + */ + public void setChildConnections(Collection childConnections) { + this.childConnections = childConnections; + } + } diff --git a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/connectiongroup/ConnectionGroupRESTService.java b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/connectiongroup/ConnectionGroupRESTService.java index aa04a5f60..ee8b79a85 100644 --- a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/connectiongroup/ConnectionGroupRESTService.java +++ b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/connectiongroup/ConnectionGroupRESTService.java @@ -23,7 +23,8 @@ package org.glyptodon.guacamole.net.basic.rest.connectiongroup; import com.google.inject.Inject; -import java.util.List; +import java.util.ArrayList; +import java.util.Collection; import javax.ws.rs.Consumes; import javax.ws.rs.DELETE; import javax.ws.rs.GET; @@ -37,12 +38,15 @@ import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response.Status; import org.glyptodon.guacamole.GuacamoleClientException; import org.glyptodon.guacamole.GuacamoleException; +import org.glyptodon.guacamole.GuacamoleResourceNotFoundException; +import org.glyptodon.guacamole.net.auth.Connection; import org.glyptodon.guacamole.net.auth.ConnectionGroup; import org.glyptodon.guacamole.net.auth.Directory; import org.glyptodon.guacamole.net.auth.UserContext; import org.glyptodon.guacamole.net.basic.rest.AuthProviderRESTExposure; import org.glyptodon.guacamole.net.basic.rest.HTTPException; import org.glyptodon.guacamole.net.basic.rest.auth.AuthenticationService; +import org.glyptodon.guacamole.net.basic.rest.connection.APIConnection; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -68,55 +72,101 @@ public class ConnectionGroupRESTService { private AuthenticationService authenticationService; /** - * A service for managing the REST endpoint APIConnection objects. + * Retrieves the given connection group from the user context, including + * all descendant connections and groups if requested. + * + * @param userContext + * The user context from which to retrieve the connection group. + * + * @param identifier + * The unique identifier of the connection group to retrieve. + * + * @param includeDescendants + * Whether the descendant connections and groups of the given + * connection group should also be retrieved. + * + * @return + * The requested connection group, or null if no such connection group + * exists. + * + * @throws GuacamoleException + * If an error occurs while retrieving the requested connection group + * or any of its descendants. */ - @Inject - private ConnectionGroupService connectionGroupService; - - /** - * The ID that will be guaranteed to refer to the root connection group. - */ - private static final String ROOT_CONNECTION_GROUP_ID = "ROOT"; - - /** - * Gets a list of connection groups with the given ConnectionGroup parentID. - * If no parentID is provided, returns the connection groups from the root group. - * - * @param authToken The authentication token that is used to authenticate - * the user performing the operation. - * @param parentID The ID of the ConnectionGroup the connection groups - * belong to. If null, the root connection group will be used. - * @return The connection list. - * @throws GuacamoleException If a problem is encountered while listing connection groups. - */ - @GET - @AuthProviderRESTExposure - public List getConnectionGroups(@QueryParam("token") String authToken, @QueryParam("parentID") String parentID) + private APIConnectionGroup retrieveConnectionGroup(UserContext userContext, + String identifier, boolean includeDescendants) throws GuacamoleException { - UserContext userContext = authenticationService.getUserContext(authToken); + ConnectionGroup rootGroup = userContext.getRootConnectionGroup(); - // If the parent connection group is passed in, try to find it. - ConnectionGroup parentConnectionGroup; - if (parentID == null) - parentConnectionGroup = userContext.getRootConnectionGroup(); + ConnectionGroup connectionGroup; + + // Use root group if requested + if (identifier == null || identifier.equals(APIConnectionGroup.ROOT_IDENTIFIER)) + connectionGroup = rootGroup; + // Otherwise, query requested group using root group directory else { - ConnectionGroup rootGroup = userContext.getRootConnectionGroup(); - Directory connectionGroupDirectory = rootGroup.getConnectionGroupDirectory(); - parentConnectionGroup = connectionGroupDirectory.get(parentID); + + Directory connectionGroupDirectory = + rootGroup.getConnectionGroupDirectory(); + + // Get the connection group from root directory + connectionGroup = connectionGroupDirectory.get(identifier); + if (connectionGroup == null) + return null; + } - if (parentConnectionGroup == null) - throw new HTTPException(Status.NOT_FOUND, "No connection group found with the provided parentID."); + // Wrap queried connection group + APIConnectionGroup apiConnectionGroup = new APIConnectionGroup(connectionGroup); - Directory connectionGroupDirectory = - parentConnectionGroup.getConnectionGroupDirectory(); + // Recursively query all descendants if necessary + if (includeDescendants) { + + // Query all child connections + Collection apiConnections = new ArrayList(); + Directory connectionDirectory = connectionGroup.getConnectionDirectory(); + + for (String childIdentifier : connectionDirectory.getIdentifiers()) { + + // Pull current connection - silently ignore if connection was removed prior to read + Connection childConnection = connectionDirectory.get(childIdentifier); + if (childConnection == null) + continue; + + apiConnections.add(new APIConnection(childConnection)); + + } + + // Associate child connections with current connection group + apiConnectionGroup.setChildConnections(apiConnections); + + // Query all child connection groups + Collection apiConnectionGroups = new ArrayList(); + Directory groupDirectory = connectionGroup.getConnectionGroupDirectory(); + + for (String childIdentifier : groupDirectory.getIdentifiers()) { + + // Pull current connection group - silently ignore if connection group was removed prior to read + APIConnectionGroup childConnectionGroup = retrieveConnectionGroup(userContext, childIdentifier, true); + if (childConnectionGroup == null) + continue; + + apiConnectionGroups.add(childConnectionGroup); + + } + + // Associate child groups with current connection group + apiConnectionGroup.setChildConnectionGroups(apiConnectionGroups); + + } + + // Return the connectiion group + return apiConnectionGroup; - // Return the converted connection group list - return connectionGroupService.convertConnectionGroupList(connectionGroupDirectory); } - + /** * Gets an individual connection group. * @@ -133,27 +183,50 @@ public class ConnectionGroupRESTService { @PathParam("connectionGroupID") String connectionGroupID) throws GuacamoleException { UserContext userContext = authenticationService.getUserContext(authToken); - - // Get the connection group directory - ConnectionGroup rootGroup = userContext.getRootConnectionGroup(); - - // Return the root group if it was asked for - if (connectionGroupID != null && connectionGroupID.equals(ROOT_CONNECTION_GROUP_ID)) - return new APIConnectionGroup(rootGroup); - - Directory connectionGroupDirectory = - rootGroup.getConnectionGroupDirectory(); - // Get the connection group - ConnectionGroup connectionGroup = connectionGroupDirectory.get(connectionGroupID); + // Retrieve requested connection group only + APIConnectionGroup connectionGroup = retrieveConnectionGroup(userContext, connectionGroupID, false); if (connectionGroup == null) - throw new HTTPException(Status.NOT_FOUND, "No ConnectionGroup found with the provided ID."); + throw new GuacamoleResourceNotFoundException("No such connection group: \"" + connectionGroupID + "\""); - // Return the connectiion group - return new APIConnectionGroup(connectionGroup); + return connectionGroup; } - + + /** + * Gets an individual connection group and all children. + * + * @param authToken + * The authentication token that is used to authenticate the user + * performing the operation. + * + * @param connectionGroupID + * The ID of the ConnectionGroup. + * + * @return + * The connection group. + * + * @throws GuacamoleException + * If a problem is encountered while retrieving the connection group or + * its descendants. + */ + @GET + @Path("/{connectionGroupID}/tree") + @AuthProviderRESTExposure + public APIConnectionGroup getConnectionGroupTree(@QueryParam("token") String authToken, + @PathParam("connectionGroupID") String connectionGroupID) throws GuacamoleException { + + UserContext userContext = authenticationService.getUserContext(authToken); + + // Retrieve requested connection group and all descendants + APIConnectionGroup connectionGroup = retrieveConnectionGroup(userContext, connectionGroupID, true); + if (connectionGroup == null) + throw new GuacamoleResourceNotFoundException("No such connection group: \"" + connectionGroupID + "\""); + + return connectionGroup; + + } + /** * Deletes an individual connection group. * @@ -174,7 +247,7 @@ public class ConnectionGroupRESTService { ConnectionGroup rootGroup = userContext.getRootConnectionGroup(); // Use the root group if it was asked for - if (connectionGroupID != null && connectionGroupID.equals(ROOT_CONNECTION_GROUP_ID)) + if (connectionGroupID != null && connectionGroupID.equals(APIConnectionGroup.ROOT_IDENTIFIER)) connectionGroupID = rootGroup.getIdentifier(); Directory connectionGroupDirectory = @@ -263,7 +336,7 @@ public class ConnectionGroupRESTService { ConnectionGroup rootGroup = userContext.getRootConnectionGroup(); // Use the root group if it was asked for - if (connectionGroupID != null && connectionGroupID.equals(ROOT_CONNECTION_GROUP_ID)) + if (connectionGroupID != null && connectionGroupID.equals(APIConnectionGroup.ROOT_IDENTIFIER)) connectionGroupID = rootGroup.getIdentifier(); Directory connectionGroupDirectory = @@ -300,7 +373,7 @@ public class ConnectionGroupRESTService { ConnectionGroup rootGroup = userContext.getRootConnectionGroup(); // Use the root group if it was asked for - if (connectionGroupID != null && connectionGroupID.equals(ROOT_CONNECTION_GROUP_ID)) + if (connectionGroupID != null && connectionGroupID.equals(APIConnectionGroup.ROOT_IDENTIFIER)) connectionGroupID = rootGroup.getIdentifier(); Directory connectionGroupDirectory = diff --git a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/connectiongroup/ConnectionGroupService.java b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/connectiongroup/ConnectionGroupService.java deleted file mode 100644 index ecbe5fffd..000000000 --- a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/connectiongroup/ConnectionGroupService.java +++ /dev/null @@ -1,59 +0,0 @@ -/* - * 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. - */ - -package org.glyptodon.guacamole.net.basic.rest.connectiongroup; - -import java.util.ArrayList; -import java.util.List; -import org.glyptodon.guacamole.GuacamoleException; -import org.glyptodon.guacamole.net.auth.ConnectionGroup; -import org.glyptodon.guacamole.net.auth.Directory; - -/** - * A service for performing useful manipulations on REST ConnectionGroups. - * - * @author James Muehlner - */ -public class ConnectionGroupService { - - /** - * Converts a ConnectionGroup directory to a list of APIConnectionGroup - * objects for exposing with the REST endpoints. - * - * @param connectionGroupDirectory The ConnectionGroup Directory to convert for REST endpoint use. - * @return A List of APIConnectionGroup objects for use with the REST endpoint. - * @throws GuacamoleException If an error occurs while converting the - * connection group directory. - */ - public List convertConnectionGroupList( - Directory connectionGroupDirectory) throws GuacamoleException { - - List restConnectionGroups = new ArrayList(); - - for (String connectionGroupID : connectionGroupDirectory.getIdentifiers()) - restConnectionGroups.add(new APIConnectionGroup(connectionGroupDirectory.get(connectionGroupID))); - - return restConnectionGroups; - - } - -} diff --git a/guacamole/src/main/webapp/app/rest/services/connectionGroupService.js b/guacamole/src/main/webapp/app/rest/services/connectionGroupService.js index a2f23dfc3..d3f3ff68b 100644 --- a/guacamole/src/main/webapp/app/rest/services/connectionGroupService.js +++ b/guacamole/src/main/webapp/app/rest/services/connectionGroupService.js @@ -29,26 +29,26 @@ angular.module('rest').factory('connectionGroupService', ['$http', 'authenticati var service = {}; /** - * Makes a request to the REST API to get the list of connection groups, - * returning a promise that provides an array of - * @link{ConnectionGroup} objects if successful. + * Makes a request to the REST API to get an individual connection group + * and all descendants, returning a promise that provides the corresponding + * @link{ConnectionGroup} if successful. Descendant groups and connections + * will be stored as children of that connection group. * - * @param {String} [parentID=ConnectionGroup.ROOT_IDENTIFIER] - * The ID of the connection group whose child connection groups should - * be returned. If not provided, the root connection group will be - * used by default. + * @param {String} [connectionGroupID=ConnectionGroup.ROOT_IDENTIFIER] + * The ID of the connection group to retrieve. If not provided, the + * root connection group will be retrieved by default. * - * @returns {Promise.} - * A promise which will resolve with an array of @link{ConnectionGroup} - * objects upon success. + * @returns {Promise.ConnectionGroup} + * A promise which will resolve with a @link{ConnectionGroup} upon + * success. */ - service.getConnectionGroups = function getConnectionGroups(parentID) { + service.getConnectionGroupTree = function getConnectionGroupTree(connectionGroupID) { - var parentIDParam = ""; - if (parentID) - parentIDParam = "&parentID=" + parentID; + // Use the root connection group ID if no ID is passed in + connectionGroupID = connectionGroupID || ConnectionGroup.ROOT_IDENTIFIER; - return $http.get("api/connectionGroup?token=" + authenticationService.getCurrentToken() + parentIDParam); + return $http.get("api/connectionGroup/" + connectionGroupID + "/tree?token=" + authenticationService.getCurrentToken()); + }; /** @@ -70,6 +70,7 @@ angular.module('rest').factory('connectionGroupService', ['$http', 'authenticati connectionGroupID = connectionGroupID || ConnectionGroup.ROOT_IDENTIFIER; return $http.get("api/connectionGroup/" + connectionGroupID + "?token=" + authenticationService.getCurrentToken()); + }; /** diff --git a/guacamole/src/main/webapp/app/rest/types/ConnectionGroup.js b/guacamole/src/main/webapp/app/rest/types/ConnectionGroup.js index f2c120c27..89e53c6eb 100644 --- a/guacamole/src/main/webapp/app/rest/types/ConnectionGroup.js +++ b/guacamole/src/main/webapp/app/rest/types/ConnectionGroup.js @@ -73,6 +73,24 @@ angular.module('rest').factory('ConnectionGroup', [function defineConnectionGrou */ this.type = template.type || ConnectionGroup.Type.ORGANIZATIONAL; + /** + * An array of all child connections, if known. This property may be + * null or undefined if children have not been queried, and thus the + * child connections are unknown. + * + * @type Connection[] + */ + this.childConnections = template.childConnections; + + /** + * An array of all child connection groups, if known. This property may + * be null or undefined if children have not been queried, and thus the + * child connection groups are unknown. + * + * @type ConnectionGroup[] + */ + this.childConnectionGroups = template.childConnectionGroups; + }; /**