/* * RHQ Management Platform * Copyright (C) 2005-2012 Red Hat, Inc. * All rights reserved. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation version 2 of the License. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. */ package org.rhq.coregui.client.inventory.resource.detail; import java.util.ArrayList; import java.util.EnumSet; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import com.google.gwt.user.client.rpc.AsyncCallback; import com.smartgwt.client.data.DSRequest; import com.smartgwt.client.data.DSResponse; import com.smartgwt.client.data.DataSource; import com.smartgwt.client.data.DataSourceField; import com.smartgwt.client.data.fields.DataSourceTextField; import com.smartgwt.client.rpc.RPCResponse; import com.smartgwt.client.types.DSDataFormat; import com.smartgwt.client.types.DSProtocol; import com.smartgwt.client.widgets.tree.Tree; import com.smartgwt.client.widgets.tree.TreeGrid; import com.smartgwt.client.widgets.tree.TreeNode; import org.rhq.core.domain.criteria.ResourceCriteria; import org.rhq.core.domain.resource.Resource; import org.rhq.core.domain.resource.ResourceType; import org.rhq.core.domain.util.PageOrdering; import org.rhq.core.domain.util.ResourceTypeUtility; import org.rhq.coregui.client.CoreGUI; import org.rhq.coregui.client.Messages; import org.rhq.coregui.client.ViewChangedException; import org.rhq.coregui.client.components.tree.EnhancedTreeNode; import org.rhq.coregui.client.gwt.GWTServiceLookup; import org.rhq.coregui.client.gwt.ResourceGWTServiceAsync; import org.rhq.coregui.client.inventory.resource.type.ResourceTypeRepository; import org.rhq.coregui.client.util.Log; import org.rhq.coregui.client.util.StringUtility; /** * This doesn't extend RPCDataSource because it is tree-oriented and behaves differently than normal list data sources * in some places. * * @author Greg Hinkle * @author Ian Springer */ public class ResourceTreeDatasource extends DataSource { private static final Messages MSG = CoreGUI.getMessages(); private List<Resource> initialData; private List<Resource> lockedData; // the encompassing grid. It's unfortunate to have the DS know about the encompassing TreeGrid // but we have a situation in which a new AG node needs to be able to access its parent TreeNode by ID. private TreeGrid treeGrid; private ResourceGWTServiceAsync resourceService = GWTServiceLookup.getResourceService(); public ResourceTreeDatasource(List<Resource> initialData, List<Resource> lockedData, TreeGrid treeGrid) { this.setClientOnly(false); this.setDataProtocol(DSProtocol.CLIENTCUSTOM); this.setDataFormat(DSDataFormat.CUSTOM); this.initialData = initialData; this.lockedData = (null != lockedData) ? lockedData : new ArrayList<Resource>(); this.treeGrid = treeGrid; DataSourceField idDataField = new DataSourceTextField("id", MSG.common_title_id()); idDataField.setPrimaryKey(true); DataSourceTextField nameDataField = new DataSourceTextField("name", MSG.common_title_name()); nameDataField.setCanEdit(false); DataSourceTextField descriptionDataField = new DataSourceTextField("description", MSG.common_title_description()); descriptionDataField.setCanEdit(false); DataSourceTextField parentIdField = new DataSourceTextField("parentId", MSG.common_title_id_parent()); parentIdField.setForeignKey("id"); this.setDropExtraFields(false); this.setFields(idDataField, nameDataField, descriptionDataField); } @Override protected Object transformRequest(DSRequest request) { String requestId = request.getRequestId(); DSResponse response = new DSResponse(); response.setAttribute("clientContext", request.getAttributeAsObject("clientContext")); // Assume success response.setStatus(0); switch (request.getOperationType()) { case ADD: //executeAdd(request, response); break; case FETCH: executeFetch(requestId, request, response); break; case REMOVE: //executeRemove(lstRec); break; case UPDATE: //executeAdd(lstRec, false); break; default: break; } return request.getData(); } public void executeFetch(final String requestId, final DSRequest request, final DSResponse response) { //final long start = System.currentTimeMillis(); CoreGUI.showBusy(true); final String parentResourceId = request.getCriteria().getAttribute("parentId"); //com.allen_sauer.gwt.log.client.Log.info("All attributes: " + Arrays.toString(request.getCriteria().getAttributes())); if (parentResourceId == null) { // If this gets called more than once it's a problem. Don't load initial data more than once. // Subsequent fetches should be due to parent node tree expansion if (null != this.initialData) { Log.debug("ResourceTreeDatasource: Loading initial data..."); List<Resource> temp = this.initialData; this.initialData = null; processIncomingData(temp, response, requestId); } else { response.setStatus(DSResponse.STATUS_FAILURE); processResponse(requestId, response); } CoreGUI.showBusy(false); } else { Log.debug(request.getCriteria().toString()); Log.debug("ResourceTreeDatasource: Loading Resource [" + parentResourceId + "]..."); // This fetch limits the number of resources that can be returned to protect against fetching a massive // number of children for a parent. Doing so may cause an unacceptably slow tree rendering, too much vertical // scroll, or perhaps even hang the gui if it consumed too many resources. To see all children the // user will need to visit the Inventory->Children view for the resource. ResourceCriteria criteria = new ResourceCriteria(); criteria.addFilterParentResourceId(Integer.parseInt(parentResourceId)); // we must sort the results to ensure that if cropped we at least show the same results each time criteria.addSortName(PageOrdering.ASC); resourceService.findResourcesByCriteriaBounded(criteria, -1, -1, new AsyncCallback<List<Resource>>() { public void onFailure(Throwable caught) { CoreGUI.getErrorHandler().handleError(MSG.view_tree_common_loadFailed_children(), caught); response.setStatus(RPCResponse.STATUS_FAILURE); processResponse(requestId, response); CoreGUI.showBusy(false); } public void onSuccess(List<Resource> result) { processIncomingData(result, response, requestId); } }); } } private void processIncomingData(List<Resource> result, final DSResponse response, final String requestId) { ResourceTypeRepository.Cache.getInstance().loadResourceTypes( result, EnumSet.of(ResourceTypeRepository.MetadataType.operations, ResourceTypeRepository.MetadataType.children), new ResourceTypeRepository.ResourceTypeLoadedCallback() { public void onResourceTypeLoaded(List<Resource> result) { TreeNode[] treeNodes = buildNodes(result, lockedData, treeGrid); response.setData(treeNodes); processResponse(requestId, response); CoreGUI.showBusy(false); } }); } /** * Construct a set of TreeNodes from a list of resources * * @param resources * @return */ public static TreeNode[] buildNodes(List<Resource> resources, List<Resource> lockedData, TreeGrid treeGrid) { if (treeGrid == null || treeGrid.getTree() == null) { throw new ViewChangedException(ResourceTopView.VIEW_ID.getName() + "/*"); } List<ResourceTreeNode> resourceNodes = new ArrayList<ResourceTreeNode>(resources.size()); for (Resource resource : resources) { ResourceTreeNode node = new ResourceTreeNode(resource, lockedData.contains(resource)); resourceNodes.add(node); } List<TreeNode> result = introduceTypeAndCategoryNodes(resourceNodes, treeGrid); return result.toArray(new TreeNode[result.size()]); } /** * @param resourceNodes ordered such that referenced parent nodes have lower indexes than the referencing child. * @return a new List, properly ordered and including AG and Subcategory nodes. */ private static List<TreeNode> introduceTypeAndCategoryNodes(final List<ResourceTreeNode> resourceNodes, TreeGrid treeGrid) { // The resulting list of nodes, including AG and SC nodes. The list is ordered to ensure all // referenced parent nodes have lower indexes than the referencing child. List<TreeNode> allNodes = new ArrayList<TreeNode>(resourceNodes.size()); // Keep track of the node IDs added so far to ensure we don't add the same node more than once. Note // that the list of resourceNodes passed in may have duplicates as the caller may not be able to // ensure a clean set. Set<String> allNodeIds = new HashSet<String>(resourceNodes.size() * 2); Map<String, Map<String, AutoGroupTreeNode>> parentNodeIdToAutoGroupsByName = new HashMap<String, Map<String, AutoGroupTreeNode>>(); Set<AutoGroupTreeNode> ambiguouslyNamedAutoGroupNodes = new HashSet<AutoGroupTreeNode>(); for (ResourceTreeNode resourceNode : resourceNodes) { if (allNodeIds.contains(resourceNode.getID())) { Log.debug("Duplicate ResourceTreeNode - Skipping: " + resourceNode); continue; } Resource resource = resourceNode.getResource(); if (resourceNode.isParentSubCategory()) { // If the parent node is a subcategory node, make sure the subcategory node is in the // tree prior to the resource node. Note that it could itself be a tree of subcategories. addSubCategoryNodes(allNodes, allNodeIds, resource); } else if (resourceNode.isParentAutoGroup()) { // If the parent node is an autogroup node, make sure the autogroup node is in the // tree prior to the resource node. // First we need to ensure we have a properly populated parentResource (id and name, minimally), // get this from the parent ResourceTreeNode as resource.parentResource may not be set with // anything more than the id. Resource parentResource = resource.getParentResource(); String parentResourceNodeId = ResourceTreeNode.idOf(parentResource); Tree tree = treeGrid.getTree(); TreeNode parentResourceNode = tree.findById(parentResourceNodeId); if (null != parentResourceNode) { parentResource = ((ResourceTreeNode) parentResourceNode).getResource(); resource.setParentResource(parentResource); } if (null == parentResource.getName()) { Log.error("AutoGroup node creation using invalid parent resource: " + parentResource); } String autoGroupNodeID = resourceNode.getParentID(); if (!allNodeIds.contains(autoGroupNodeID)) { AutoGroupTreeNode autogroupNode = new AutoGroupTreeNode(resource); String parentID = autogroupNode.getParentID(); Map<String, AutoGroupTreeNode> autoGroupNodesByName = parentNodeIdToAutoGroupsByName.get(parentID); if (autoGroupNodesByName == null) { autoGroupNodesByName = new HashMap<String, AutoGroupTreeNode>(); parentNodeIdToAutoGroupsByName.put(parentID, autoGroupNodesByName); } else { AutoGroupTreeNode ambiguouslyNamedAutogroupNode = autoGroupNodesByName.get(autogroupNode .getName()); if (ambiguouslyNamedAutogroupNode != null) { ambiguouslyNamedAutoGroupNodes.add(ambiguouslyNamedAutogroupNode); ambiguouslyNamedAutoGroupNodes.add(autogroupNode); } } autoGroupNodesByName.put(autogroupNode.getName(), autogroupNode); if (autogroupNode.isParentSubcategory()) { // If the parent node of the autogroup node is a subcategory node, make sure the subcategory // node is in the tree prior to the autogroup node. Note that it could itself be a // tree of subcategories. addSubCategoryNodes(allNodes, allNodeIds, resource); } allNodeIds.add(autoGroupNodeID); allNodes.add(autogroupNode); } } allNodeIds.add(resourceNode.getID()); allNodes.add(resourceNode); } for (AutoGroupTreeNode autogroupNode : ambiguouslyNamedAutoGroupNodes) { autogroupNode.disambiguateName(); } return allNodes; } // convenience routine to avoid code duplication private static void addSubCategoryNodes(List<TreeNode> allNodes, Set<String> allNodeIds, Resource resource) { int parentResourceId = resource.getParentResource().getId(); ResourceType type = resource.getResourceType(); String subCategoryNodeId = null; String treeParentId = ResourceTreeNode.idOf(parentResourceId); String[] subcategoryNames = type.getSubCategory().split("\\|"); String subcategoryAncestry = null; for (String subcategoryName : subcategoryNames) { if (subcategoryAncestry == null) { subcategoryAncestry = subcategoryName; } else { subcategoryAncestry = subcategoryAncestry + "|" + subcategoryName; } subCategoryNodeId = SubCategoryTreeNode.idOf(subcategoryAncestry, parentResourceId); if (!allNodeIds.contains(subCategoryNodeId)) { SubCategoryTreeNode subCategoryNode = new SubCategoryTreeNode(subcategoryName, subcategoryAncestry, parentResourceId, treeParentId); allNodeIds.add(subCategoryNodeId); allNodes.add(subCategoryNode); } treeParentId = subCategoryNodeId; } } public static class ResourceTreeNode extends EnhancedTreeNode { private Resource resource; private boolean isLocked; private boolean parentAutoGroup = false; private boolean parentSubCategory = false; /** * The parentID will be set to the parent resource at construction. It can be changed * later (prior to tree linkage) if the resource node should logically be set to an * autogroup or subcategory parent. * * @param resource The resource must have, minimally, id, name, description set. And, if parent is not null, * parentResource.id must be set as well. Also, resourceType.childresourceTypes. * @param isLocked */ private ResourceTreeNode(Resource resource, boolean isLocked) { this.resource = resource; this.isLocked = isLocked; String id = idOf(resource); setID(id); // a resource node can have any of three different parent node types; resource, subcategory or autogroup. // this can be determined at construction time so set it properly now and assume the proper node // structure will be in place later. Resource parentResource = resource.getParentResource(); String parentId = null; if (null != parentResource) { // non-singletons will always be autogrouped if (!resource.getResourceType().isSingleton()) { parentId = AutoGroupTreeNode.idOf(resource); this.parentAutoGroup = true; } else { String subcategory = resource.getResourceType().getSubCategory(); if (null != subcategory) { parentId = SubCategoryTreeNode.idOf(subcategory, parentResource.getId()); this.parentSubCategory = true; } else parentId = ResourceTreeNode.idOf(parentResource); } } this.setParentID(parentId); // name and description are user-editable, so escape HTML to prevent XSS attacks String name = resource.getName(); String escapedName = StringUtility.escapeHtml(name); setName(escapedName); String description = resource.getDescription(); String escapedDescription = StringUtility.escapeHtml(description); setAttribute(Attributes.DESCRIPTION, escapedDescription); Set<ResourceType> childTypes = resource.getResourceType().getChildResourceTypes(); setIsFolder((childTypes != null && !childTypes.isEmpty())); } public Resource getResource() { return this.resource; } public boolean isLocked() { return isLocked; } public boolean isParentAutoGroup() { return parentAutoGroup; } public boolean isParentSubCategory() { return parentSubCategory; } public static String idOf(Resource resource) { return idOf(resource.getId()); } public static String idOf(int resourceId) { return String.valueOf(resourceId); } } /** * The folder node for a Resource subCategory. */ public static class SubCategoryTreeNode extends EnhancedTreeNode { public SubCategoryTreeNode(String subcategoryName, String subcategoryAncestry, int parentResourceId, String parentTreeId) { String id = idOf(subcategoryAncestry, parentResourceId); setID(id); setParentID(parentTreeId); // Note, subCategory names are typically already plural, so there's no need to pluralize them. setName(subcategoryName); setAttribute(Attributes.DESCRIPTION, subcategoryName); } public static String idOf(String subcategoryName, int parentResourceId) { return "subcat_" + subcategoryName.hashCode() + "_" + parentResourceId; } } /** * The folder node for a Resource autogroup. */ public static class AutoGroupTreeNode extends EnhancedTreeNode { private Resource parentResource; private ResourceType resourceType; private boolean parentSubcategory = false; private Integer resourceGroupId; // set after the node is visited, otherwise null /** * @param resource resource.id must be set. resource.parentResource.id, .name must be set. * resource.resourceType.id, .name, .description, .subCategory must be set. */ private AutoGroupTreeNode(Resource resource) { this.parentResource = resource.getParentResource(); this.resourceType = resource.getResourceType(); String id = idOf(resource); setID(id); // parent node is either a subCategory node or a resource node String parentId; String subcategory = this.resourceType.getSubCategory(); if (subcategory != null) { parentId = SubCategoryTreeNode.idOf(subcategory, this.parentResource.getId()); this.parentSubcategory = true; } else { parentId = ResourceTreeNode.idOf(this.parentResource); } setParentID(parentId); String name = StringUtility.pluralize(ResourceTypeUtility.displayName(this.resourceType)); setName(name); setAttribute(Attributes.DESCRIPTION, this.resourceType.getDescription()); } public Resource getParentResource() { return parentResource; } public ResourceType getResourceType() { return resourceType; } /** * Generates a backing group name based on the resource type name and parent resource name. It may not be unique * so should not be used to query for the group (use rtId and parentResId). The name may be displayed to the * user. * * @return The name of the backing group. */ public String getBackingGroupName() { return this.getParentResource().getName() + " ( " + ResourceTypeUtility.displayName(this.resourceType) + " )"; } public Integer getResourceGroupId() { return resourceGroupId; } public void setResourceGroupId(Integer resourceGroupId) { this.resourceGroupId = resourceGroupId; } public boolean isParentSubcategory() { return parentSubcategory; } /** * Given a Resource, generate a unique ID for the AGNode. * * @param resource requires resourceType field be set. requires parentResource field be set (null for no parent) * @return The name string or null if the parentResource is null. */ public static String idOf(Resource resource) { Resource parentResource = resource.getParentResource(); return idOf(parentResource, resource.getResourceType()); } /** * Given an autogroup's parent Resource and member ResourceType, generate a unique ID for an autogroup TreeNode. * * @param parentResource requires resourceType field be set. requires parentResource field be set (null for no parent) * @param resourceType the member ResourceType * * @return The name string or null if the parentResource is null */ public static String idOf(Resource parentResource, ResourceType resourceType) { return (parentResource != null) ? "autogroup_" + resourceType.getId() + "_" + parentResource.getId() : null; } public void disambiguateName() { String typeName = StringUtility.pluralize(ResourceTypeUtility.displayName(this.resourceType)); String name = typeName + " (" + this.resourceType.getPlugin() + " " + MSG.common_title_plugin().toLowerCase() + ")"; setName(name); } } }