/* * 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.groups.detail.monitoring.table; import java.util.ArrayList; import java.util.EnumSet; import java.util.Iterator; import java.util.List; import java.util.Set; import java.util.TreeSet; import com.google.gwt.user.client.Timer; import com.google.gwt.user.client.rpc.AsyncCallback; import com.smartgwt.client.widgets.HTMLFlow; import com.smartgwt.client.widgets.layout.HLayout; import org.rhq.core.domain.common.EntityContext; import org.rhq.core.domain.criteria.ResourceGroupCriteria; import org.rhq.core.domain.measurement.MeasurementDefinition; import org.rhq.core.domain.measurement.MeasurementUnits; import org.rhq.core.domain.measurement.composite.MeasurementDataNumericHighLowComposite; import org.rhq.core.domain.measurement.composite.MeasurementNumericValueAndUnits; import org.rhq.core.domain.resource.Resource; import org.rhq.core.domain.resource.ResourceType; import org.rhq.core.domain.resource.group.ResourceGroup; import org.rhq.core.domain.resource.group.composite.ResourceGroupComposite; import org.rhq.core.domain.util.PageList; import org.rhq.coregui.client.CoreGUI; import org.rhq.coregui.client.JsonMetricProducer; import org.rhq.coregui.client.Messages; import org.rhq.coregui.client.dashboard.AutoRefreshUtil; import org.rhq.coregui.client.gwt.GWTServiceLookup; import org.rhq.coregui.client.gwt.ResourceGroupGWTServiceAsync; import org.rhq.coregui.client.inventory.AutoRefresh; import org.rhq.coregui.client.inventory.common.graph.ButtonBarDateTimeRangeEditor; import org.rhq.coregui.client.inventory.common.graph.Refreshable; import org.rhq.coregui.client.inventory.resource.type.ResourceTypeRepository; import org.rhq.coregui.client.util.Log; import org.rhq.coregui.client.util.MeasurementConverterClient; import org.rhq.coregui.client.util.async.Command; import org.rhq.coregui.client.util.async.CountDownLatch; import org.rhq.coregui.client.util.enhanced.EnhancedHLayout; import org.rhq.coregui.client.util.enhanced.EnhancedVLayout; import org.rhq.coregui.client.util.message.Message; /** * This composite graph view has different graph types and data structures for * graphing multiple individual resources of the composite resource as * multi-line graph. Single Metric Multiple Resource graph. * * @author Mike Thompson */ public abstract class CompositeGroupD3GraphListView extends EnhancedVLayout implements JsonMetricProducer, AutoRefresh, Refreshable { static protected final Messages MSG = CoreGUI.getMessages(); // The d3 color palette onlys supports 20 colors so we limit to this private static final int MAX_NUMBER_OF_LINES_IN_GRAPH = 50; // string labels private final String chartTitleMinLabel = MSG.chart_title_min_label(); private final String chartTitleAvgLabel = MSG.chart_title_avg_label(); private final String chartTitlePeakLabel = MSG.chart_title_peak_label(); private final String chartDateLabel = MSG.chart_date_label(); private final String chartTimeLabel = MSG.chart_time_label(); private final String chartHoverTimeFormat = MSG.chart_hover_time_format(); private final String chartHoverDateFormat = MSG.chart_hover_date_format(); private EntityContext context; private int definitionId; private MeasurementDefinition definition; private ButtonBarDateTimeRangeEditor buttonBarDateTimeRangeEditor; private MeasurementUnits adjustedMeasurementUnits; private Set<Resource> childResources; /** * measurementForEachResource is a list of a list of single Measurement data for multiple resources. */ private List<MultiLineGraphData> measurementForEachResource; private HLayout titleHLayout; private HTMLFlow graph; private String chartTitle; private Integer chartHeight; protected static Timer refreshTimer; protected boolean isRefreshing; public CompositeGroupD3GraphListView(EntityContext context, int defId) { super(); this.context = context; setDefinitionId(defId); measurementForEachResource = new ArrayList<MultiLineGraphData>(); buttonBarDateTimeRangeEditor = new ButtonBarDateTimeRangeEditor(this); setHeight100(); setWidth100(); setPadding(10); startRefreshCycle(); } public void populateData() { ResourceGroupGWTServiceAsync groupService = GWTServiceLookup.getResourceGroupService(); ResourceGroupCriteria criteria = new ResourceGroupCriteria(); criteria.addFilterId(context.getGroupId()); criteria.fetchResourceType(true); criteria.fetchExplicitResources(true); if (context.isAutoCluster()) { criteria.addFilterVisible(false); } else if (context.isAutoGroup()) { criteria.addFilterVisible(false); criteria.addFilterPrivate(true); } childResources = new TreeSet<Resource>(); measurementForEachResource.clear(); groupService.findResourceGroupCompositesByCriteria(criteria, new AsyncCallback<PageList<ResourceGroupComposite>>() { @Override public void onFailure(Throwable caught) { CoreGUI.getErrorHandler().handleError(MSG.view_resource_monitor_graphs_lookupFailed(), caught); } @Override public void onSuccess(PageList<ResourceGroupComposite> result) { if (result.isEmpty()) { return; } final ResourceGroup parentGroup = result.get(0).getResourceGroup(); //Limit the number of child lines in graph due to 20 colors being supported in d3 graph palette final Set<Resource> fullSetOfChildResources = parentGroup.getExplicitResources(); int i = 0; for (Iterator<Resource> resourceIterator = fullSetOfChildResources.iterator(); resourceIterator.hasNext(); ) { Resource nextChildResource = resourceIterator.next(); if(i >= MAX_NUMBER_OF_LINES_IN_GRAPH){ CoreGUI.getMessageCenter().notify(new Message( MSG.chart_warning_max_composite_graphs_limit_exceeded()+" "+String.valueOf( MAX_NUMBER_OF_LINES_IN_GRAPH ), Message.Severity.Warning)); break; } childResources.add(nextChildResource); i++; } chartTitle = parentGroup.getName(); Log.info("group name: " + parentGroup.getName() + ", size: " + parentGroup.getExplicitResources().size()); // setting up a deferred Command to execute after all resource queries have completed (successfully or unsuccessfully) final CountDownLatch countDownLatch = CountDownLatch.create(childResources.size(), new Command() { @Override /** * Do this only after ALL of the metric queries for each resource */ public void execute() { if (parentGroup.getExplicitResources().size() != measurementForEachResource.size()) { Log.warn("Number of graphs doesn't match number of resources"); Log.warn("# of child resources: " + parentGroup.getExplicitResources().size()); Log.warn("# of charted graphs: " + measurementForEachResource.size()); } drawGraph(); } }); if (!childResources.isEmpty()) { // resourceType will be the same for all autogroup children so get first Resource childResource = childResources.iterator().next(); ResourceTypeRepository.Cache.getInstance().getResourceTypes( childResource.getResourceType().getId(), EnumSet.of(ResourceTypeRepository.MetadataType.measurements), new ResourceTypeRepository.TypeLoadedCallback() { @Override public void onTypesLoaded(final ResourceType type) { for (MeasurementDefinition def : type.getMetricDefinitions()) { // only need the one selected measurement if (def.getId() == getDefinitionId()) { setDefinition(def); } } buttonBarDateTimeRangeEditor.updateTimeRangeToNow(); for (final Resource childResource : childResources) { Log.debug("Adding child composite: " + childResource.getName() + childResource.getId()); GWTServiceLookup.getMeasurementDataService().findDataForResource( childResource.getId(), new int[]{getDefinitionId()}, buttonBarDateTimeRangeEditor.getStartTime(), buttonBarDateTimeRangeEditor.getEndTime(), 60, new AsyncCallback<List<List<MeasurementDataNumericHighLowComposite>>>() { @Override public void onFailure(Throwable caught) { CoreGUI.getErrorHandler().handleError( MSG.view_resource_monitor_graphs_loadFailed(), caught); countDownLatch.countDown(); } @Override public void onSuccess( List<List<MeasurementDataNumericHighLowComposite>> measurements) { addMeasurementForEachResource(childResource.getName(), childResource.getId(), measurements.get(0)); countDownLatch.countDown(); } }); } } }); } } }); } /** * Adding is done asynchronously, so we must synchronize the add. * @param resourceMeasurementList */ public synchronized void addMeasurementForEachResource(String resourceName, int resourceId, List<MeasurementDataNumericHighLowComposite> resourceMeasurementList) { measurementForEachResource.add(new MultiLineGraphData(resourceName, resourceId, resourceMeasurementList)); } @Override protected void onDraw() { super.onDraw(); drawGraph(); } @Override public void parentResized() { super.parentResized(); removeMembers(getMembers()); drawGraph(); } public int getDefinitionId() { return definitionId; } public void setDefinitionId(int definitionId) { this.definitionId = definitionId; this.definition = null; } public String getChartId() { return String.valueOf(definition.getId()); } public void setDefinition(MeasurementDefinition definition) { this.definition = definition; } private void removeMembers() { removeMember(buttonBarDateTimeRangeEditor); if (null != titleHLayout) removeMember(titleHLayout); if (null != graph) removeMember(graph); } @Override public void refreshData() { populateData(); drawGraph(); } private void drawGraph() { Log.debug("drawGraph in CompositeGroupD3GraphListView for: " + definition + " (" + definitionId + ")"); if (null != titleHLayout) { removeMembers(); } addMember(buttonBarDateTimeRangeEditor); titleHLayout = new EnhancedHLayout(); if (definition != null) { titleHLayout.setAutoHeight(); titleHLayout.setWidth100(); addMember(titleHLayout); graph = new HTMLFlow("<div id=\"mChart-" + getChartId() + "\" ><svg xmlns=\"http://www.w3.org/2000/svg\" version=\"1.1\" style=\"height:95%;\"></svg></div>"); graph.setWidth100(); graph.setHeight100(); addMember(graph); new Timer(){ @Override public void run() { buttonBarDateTimeRangeEditor.updateTimeRangeToNow(); drawJsniChart(); } }.schedule(200); } } public String getYAxisTitle() { return definition.getName(); } public String getXAxisTitle() { return MSG.view_charts_time_axis_label(); } public String getChartTitleMinLabel() { return chartTitleMinLabel; } public String getChartTitleAvgLabel() { return chartTitleAvgLabel; } public String getChartTitlePeakLabel() { return chartTitlePeakLabel; } public String getChartDateLabel() { return chartDateLabel; } public String getChartTimeLabel() { return chartTimeLabel; } public String getChartHoverTimeFormat() { return chartHoverTimeFormat; } public String getChartHoverDateFormat() { return chartHoverDateFormat; } public String getButtonBarDateTimeFormat() { return MSG.common_buttonbar_datetime_format_moment_js(); } public String getChartTitle() { return chartTitle; } public int getChartHeight() { return chartHeight != null ? chartHeight : 300; } public void setChartHeight(Integer chartHeight) { this.chartHeight = chartHeight; } private MeasurementUnits findAdjustedMeasurementUnit(List<MultiLineGraphData> measurements) { // find he highest value MeasurementDataNumericHighLowComposite highestValue = null; for (MultiLineGraphData data : measurements) { for (MeasurementDataNumericHighLowComposite measurement : data.getMeasurementData()) { if (!Double.isNaN(measurement.getValue())) { if (null == highestValue) { highestValue = measurement; } if (measurement.getValue() > highestValue.getValue()) { highestValue = measurement; } } } } if (null != highestValue) { MeasurementNumericValueAndUnits adjustedMeasurementUnitsAndValue = MeasurementConverterClient.fit( highestValue.getValue(), definition.getUnits()); Log.debug("Selected unit is " + adjustedMeasurementUnitsAndValue.getUnits().toString()); return adjustedMeasurementUnitsAndValue.getUnits(); } else { return definition.getUnits(); } } /** * Takes a measurementList for each resource and turn it into an array. * @return String */ private String produceInnerValuesArray(List<MeasurementDataNumericHighLowComposite> measurementList) { StringBuilder sb = new StringBuilder("["); for (MeasurementDataNumericHighLowComposite measurement : measurementList) { sb.append("{ \"x\":" + measurement.getTimestamp() + ","); if (!Double.isNaN(measurement.getValue())) { MeasurementNumericValueAndUnits dataValue = normalizeUnitsAndValues(measurement.getValue(), adjustedMeasurementUnits); sb.append(" \"y\":" + dataValue.getValue() + "},"); }else { sb.append(" \"y\": 0, \"nodata\": true},"); } } if (sb.length() > 1) { sb.setLength(sb.length() - 1); // delete the last ',' } sb.append("]"); return sb.toString(); } @Override public String getJsonMetrics() { StringBuilder sb = new StringBuilder(); if (null != measurementForEachResource && !measurementForEachResource.isEmpty()) { adjustedMeasurementUnits = findAdjustedMeasurementUnit(measurementForEachResource); sb = new StringBuilder("["); for (MultiLineGraphData multiLineGraphData : measurementForEachResource) { if(null != multiLineGraphData.getMeasurementData() && multiLineGraphData.getMeasurementData().size() > 0){ sb.append("{ \"key\": \""); sb.append(multiLineGraphData.getResourceNameEscaped()); sb.append("\",\"value\" : "); sb.append(produceInnerValuesArray(multiLineGraphData.getMeasurementData())); sb.append("},"); } } sb.setLength(sb.length() - 1); // delete the last ',' sb.append("]"); Log.debug("Multi-resource Graph size: " + measurementForEachResource.size()); } //Log.debug("Multi-resource Graph json: " + sb.toString()); return sb.toString(); } protected MeasurementNumericValueAndUnits normalizeUnitsAndValues(double value, MeasurementUnits measurementUnits) { Double scaledValue = MeasurementConverterClient.scale( new MeasurementNumericValueAndUnits(value, definition.getUnits()), adjustedMeasurementUnits); MeasurementNumericValueAndUnits newValue = new MeasurementNumericValueAndUnits(scaledValue, measurementUnits); MeasurementNumericValueAndUnits returnValue; // adjust for percentage numbers if (measurementUnits.equals(MeasurementUnits.PERCENTAGE)) { returnValue = new MeasurementNumericValueAndUnits(newValue.getValue() * 100, newValue.getUnits()); } else { returnValue = new MeasurementNumericValueAndUnits(newValue.getValue(), newValue.getUnits()); } return returnValue; } public String getYAxisUnits() { if (adjustedMeasurementUnits == null) { Log.warn("ResourceMetricD3GraphView.adjustedMeasurementUnits is populated by getJsonMetrics. Make sure it is called first."); return ""; } else { return adjustedMeasurementUnits.toString(); } } protected String getXAxisTimeFormatHoursMinutes() { return MSG.chart_xaxis_time_format_hours_minutes(); } protected String getXAxisTimeFormatHours() { return MSG.chart_xaxis_time_format_hours(); } @Override public void startRefreshCycle() { refreshTimer = AutoRefreshUtil.startRefreshCycleWithPageRefreshInterval(this, this, refreshTimer); } @Override protected void onDestroy() { AutoRefreshUtil.onDestroy(refreshTimer); super.onDestroy(); } @Override public boolean isRefreshing() { return isRefreshing; } //Custom refresh operation as we are not directly extending Table @Override public void refresh() { if (isVisible() && !isRefreshing()) { isRefreshing = true; try { refreshData(); } finally { isRefreshing = false; } } } /** * Client can choose which graph types to render. */ public abstract void drawJsniChart(); /** * Immutable data for each graph line. */ private final class MultiLineGraphData { private String resourceName; private int resourceId; private List<MeasurementDataNumericHighLowComposite> measurementData; private MultiLineGraphData(String resourceName, int resourceId, List<MeasurementDataNumericHighLowComposite> measurmentData) { this.resourceName = resourceName; this.resourceId = resourceId; this.measurementData = measurmentData; } public String getResourceName() { return resourceName; } public String getResourceNameEscaped() { return resourceName.replace('"', '\''); } public int getResourceId() { return resourceId; } public List<MeasurementDataNumericHighLowComposite> getMeasurementData() { return measurementData; } } }