// Copyright 2010 Google Inc. All Rights Reseved. // // Licensed 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 com.google.testing.testify.risk.frontend.client.view.impl; import com.google.common.collect.HashMultimap; import com.google.common.collect.Lists; import com.google.common.collect.Maps; import com.google.common.collect.Multimap; import com.google.common.collect.Sets; import com.google.gwt.core.client.GWT; import com.google.gwt.event.dom.client.ClickEvent; import com.google.gwt.event.dom.client.ClickHandler; import com.google.gwt.event.logical.shared.HasValueChangeHandlers; import com.google.gwt.event.logical.shared.ValueChangeEvent; import com.google.gwt.event.logical.shared.ValueChangeHandler; import com.google.gwt.event.shared.HandlerRegistration; import com.google.gwt.uibinder.client.UiBinder; import com.google.gwt.uibinder.client.UiFactory; import com.google.gwt.uibinder.client.UiField; import com.google.gwt.user.client.ui.Composite; import com.google.gwt.user.client.ui.Grid; import com.google.gwt.user.client.ui.HTML; import com.google.gwt.user.client.ui.HTMLTable; import com.google.gwt.user.client.ui.HTMLTable.CellFormatter; import com.google.gwt.user.client.ui.Label; import com.google.gwt.user.client.ui.SimplePanel; import com.google.gwt.user.client.ui.VerticalPanel; import com.google.gwt.user.client.ui.Widget; import com.google.testing.testify.risk.frontend.client.riskprovider.RiskProvider; import com.google.testing.testify.risk.frontend.client.view.RiskView; import com.google.testing.testify.risk.frontend.client.view.widgets.EasyDisclosurePanel; import com.google.testing.testify.risk.frontend.client.view.widgets.PageSectionVerticalPanel; import com.google.testing.testify.risk.frontend.model.Attribute; import com.google.testing.testify.risk.frontend.model.Capability; import com.google.testing.testify.risk.frontend.model.CapabilityIntersectionData; import com.google.testing.testify.risk.frontend.model.Component; import com.google.testing.testify.risk.frontend.model.Pair; import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Map; /** * Base Widget for displaying Risk and/or Mitigation. (A glorified 2D heat map.) The control acts * as a repository of project Attributes, Components, and Capabilities so that future * risk providers can rely on that data to do visualization. * * @author chrsmith@google.com (Chris Smith) * @author jimr@google.com (Jim Reardon) */ public abstract class RiskViewImpl extends Composite implements RiskView, HasValueChangeHandlers<Pair<Integer, Integer>> { /** Enum for tracking the required pieces of information to fully initialize a risk view. */ private enum RequiredDataType { ATTRIBUTES, COMPONENTS, CAPABILITIES } /** * Used to wire parent class to associated UI Binder. */ interface RiskViewImplUiBinder extends UiBinder<Widget, RiskViewImpl> { } private static final RiskViewImplUiBinder uiBinder = GWT.create(RiskViewImplUiBinder.class); @UiField public PageSectionVerticalPanel pageSectionPanel; @UiField public Label introTextLabel; @UiField public Grid baseGrid; /** Panel to hold custom content from a derived class. */ @UiField public VerticalPanel content; /** Panel to hold custom content at the bottom of the widget. */ @UiField public SimplePanel bottomContent; /** * Map of intersection key to data stored inside a CapabilityIntersectionData object. */ private final Map<Integer, CapabilityIntersectionData> dataMap = Maps.newHashMap(); /** * Map of intersection key to capability list. */ private final Multimap<Integer, Capability> capabilityMap = HashMultimap.create(); private final HashSet<RequiredDataType> initializedDataTypes = Sets.newHashSet(); private final ArrayList<Component> components = Lists.newArrayList(); private final ArrayList<Attribute> attributes = Lists.newArrayList(); private Pair<Integer, Integer> selectedCell; /** * Constructs a new instance of the RiskViewImpl widget. For the UI to display something, call * {@link #setComponents(List)}, {@link #setAttributes(List)}, and {@link #setCapabilities(List)} * next. */ public RiskViewImpl() { initWidget(uiBinder.createAndBindUi(this)); setPageText("", ""); } @UiFactory public EasyDisclosurePanel createDisclosurePanel() { Label header = new Label("Risk displayed by Attribute and Component"); header.addStyleName("tty-DisclosureHeader"); return new EasyDisclosurePanel(header); } /** * Sets the risk page's introductory text. * * @param titleText the text displayed on the top, for example "Risk Factors". * @param introText the page text, explaining what the data illustrates. */ protected void setPageText(String titleText, String introText) { pageSectionPanel.setHeaderText(titleText); introTextLabel.setText(introText); } @Override public void setComponents(List<Component> components) { this.components.clear(); this.components.addAll(components); initializedDataTypes.add(RequiredDataType.COMPONENTS); initializeGrid(); initializeRiskCells(); } @Override public void setAttributes(List<Attribute> attributes) { this.attributes.clear(); this.attributes.addAll(attributes); initializedDataTypes.add(RequiredDataType.ATTRIBUTES); initializeGrid(); initializeRiskCells(); } @Override public void setCapabilities(List<Capability> newCapabilities) { capabilityMap.clear(); for (Capability capability : newCapabilities) { capabilityMap.put(capability.getCapabilityIntersectionKey(), capability); } initializedDataTypes.add(RequiredDataType.CAPABILITIES); initializeRiskCells(); } /** * Called for derived classes once the risk view has been fully initilzied. (All Attributes, * Components, and Capabilities have been specified.) */ protected abstract void onInitialized(); @Override public Widget asWidget() { return this; } /** * Initializes the Risk grid headers. */ void initializeGrid() { // We need both attributes and components for this control to make sense. if ((!initializedDataTypes.contains(RequiredDataType.ATTRIBUTES)) || (!initializedDataTypes.contains(RequiredDataType.COMPONENTS))) { return; } baseGrid.clear(); baseGrid.addClickHandler(new ClickHandler() { @Override public void onClick(ClickEvent event) { cellClicked(baseGrid.getCellForEvent(event)); } }); baseGrid.resize(components.size() + 1, attributes.size() + 1); CellFormatter formatter = baseGrid.getCellFormatter(); formatter.setStyleName(0, 0, "tty-GridXHeaderCell"); // Initialize the column and row headers. for (int cIndex = 0; cIndex < components.size(); cIndex++) { Label headerLabel = new Label(components.get(cIndex).getName()); formatter.setStyleName(cIndex + 1, 0, "tty-GridXHeaderCell"); baseGrid.setWidget(cIndex + 1, 0, headerLabel); } for (int aIndex = 0; aIndex < attributes.size(); aIndex++) { Label headerLabel = new Label(attributes.get(aIndex).getName()); formatter.setStyleName(0, aIndex + 1, "tty-GridYHeaderCell"); baseGrid.setWidget(0, aIndex + 1, headerLabel); } // Initialize the data rows. for (int cIndex = 0; cIndex < components.size(); cIndex++) { for (int aIndex = 0; aIndex < attributes.size(); aIndex++) { HTML html = new HTML(" "); baseGrid.setWidget(cIndex + 1, aIndex + 1, html); } } } /** * Determines if all information necessary for the grid has been loaded from the server. * * @return true if fully loaded, false if not. */ protected boolean isInitialized() { // Make sure all required pieces of data are available. for (RequiredDataType type : RequiredDataType.values()) { if (!initializedDataTypes.contains(type)) { return false; } } return true; } /** * Executes on clicking a cell. * * @param cell the cell clicked on. */ private void cellClicked(HTMLTable.Cell cell) { if (cell != null) { int row = cell.getRowIndex(); int column = cell.getCellIndex(); // Ignore headers. if (row > 0 && column > 0) { // Unhighlight currently selected cell. if (selectedCell != null) { baseGrid.getCellFormatter().removeStyleName(selectedCell.getFirst(), selectedCell.getSecond(), "tty-RiskCellSelected"); } baseGrid.getCellFormatter().addStyleName(row, column, "tty-RiskCellSelected"); selectedCell = new Pair<Integer, Integer>(row, column); ValueChangeEvent.fire(this, selectedCell); } } } /** * Returns the CapabilityIntersectionData for a given row and column. * * @param row the row of the table you're interested in. * @param column the column of the table you're interested in. * @return data. */ protected CapabilityIntersectionData getDataForCell(int row, int column) { int cIndex = row - 1; int aIndex = column - 1; if (aIndex < 0 || aIndex >= attributes.size() || cIndex < 0 || cIndex >= components.size()) { return null; } Attribute attribute = attributes.get(aIndex); Component component = components.get(cIndex); Integer key = Capability.getCapabilityIntersectionKey(component, attribute); return dataMap.get(key); } /** * Initialize the riskProviderCells field. (Maybe called more than once as asynchronous calls get * returned.) */ private void initializeRiskCells() { if (!isInitialized()) { return; } for (Attribute attribute : attributes) { for (Component component : components) { Integer key = Capability.getCapabilityIntersectionKey(component, attribute); CapabilityIntersectionData data = new CapabilityIntersectionData( attribute, component, capabilityMap.get(key)); dataMap.put(key, data); } } // Notify derived classes the risk view has been fully initialized, and is ready for painting. onInitialized(); } /** * Refreshes the risk data for all cells, based on the risk provided by the passed in risk * provider. * * @param provider provider that determines risk. */ protected void refreshRiskCalculation(RiskProvider provider) { if (provider == null) { return; } refreshRiskCalculation(Lists.newArrayList(provider)); } /** * Refreshes the risk data for all cells, based on the risk provided by the passed in risk * providers. * * @param providers providers that determines risk (risk is additive). */ protected void refreshRiskCalculation(List<RiskProvider> providers) { for (int cIndex = 0; cIndex < components.size(); cIndex++) { for (int aIndex = 0; aIndex < attributes.size(); aIndex++) { int row = cIndex + 1; int column = aIndex + 1; Attribute attribute = attributes.get(aIndex); Component component = components.get(cIndex); Integer key = Capability.getCapabilityIntersectionKey(component, attribute); CapabilityIntersectionData data = dataMap.get(key); double risk = 0.0; double mitigations = 0.0; // TODO(jimr): RiskProvider would be better off exposing a type instead of doing it based // off the returned positive/negative. for (RiskProvider provider : providers) { double sourceRisk = provider.calculateRisk(data); if (sourceRisk < 0) { mitigations += sourceRisk; } else { risk += sourceRisk; } } updateCell(row, column, risk, mitigations); } } } /** * Updates a cell with a new risk value. * * @param row the cell's row. * @param column the cell's column. * @param risk the new risk value. * @param mitigations the new mitigation value. */ private void updateCell(int row, int column, double risk, double mitigations) { // Mitigations and risk don't need to be separate, but this gives us flexibility in the future. double totalRisk = risk + mitigations; int intensity = (int) (totalRisk * 10.0) * 10; if (intensity > 100) { intensity = 100; } else if (intensity < -100) { intensity = -100; } String intensityCss = "tty-RiskIntensity_" + Integer.toString(intensity); baseGrid.getCellFormatter().setStyleName(row, column, "tty-GridCell"); baseGrid.getCellFormatter().addStyleName(row, column, intensityCss); } /** * Retreive a list of data for all intersection points. * * @return list of CapabilityIntersectionData objects for all intersections on the grid. */ protected List<CapabilityIntersectionData> getIntersectionData() { return Lists.newArrayList(dataMap.values()); } @Override public HandlerRegistration addValueChangeHandler( ValueChangeHandler<Pair<Integer, Integer>> handler) { return addHandler(handler, ValueChangeEvent.getType()); } }