/* * RHQ Management Platform * Copyright (C) 2005-2013 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., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA */ package org.rhq.enterprise.server.rest; import java.io.IOException; import java.io.OutputStream; import java.io.PrintWriter; import java.net.URI; import java.util.ArrayList; import java.util.Collection; import java.util.Date; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; import javax.ejb.Asynchronous; import javax.ejb.EJB; import javax.ejb.Stateless; import javax.interceptor.Interceptors; import javax.persistence.EntityManager; import javax.persistence.PersistenceContext; import javax.ws.rs.Consumes; import javax.ws.rs.DefaultValue; import javax.ws.rs.GET; import javax.ws.rs.POST; import javax.ws.rs.PUT; import javax.ws.rs.Path; import javax.ws.rs.PathParam; import javax.ws.rs.Produces; import javax.ws.rs.QueryParam; import javax.ws.rs.WebApplicationException; import javax.ws.rs.core.CacheControl; import javax.ws.rs.core.Context; import javax.ws.rs.core.EntityTag; import javax.ws.rs.core.GenericEntity; import javax.ws.rs.core.HttpHeaders; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Request; import javax.ws.rs.core.Response; import javax.ws.rs.core.Response.Status; import javax.ws.rs.core.StreamingOutput; import javax.ws.rs.core.UriBuilder; import javax.ws.rs.core.UriInfo; import com.wordnik.swagger.annotations.Api; import com.wordnik.swagger.annotations.ApiError; import com.wordnik.swagger.annotations.ApiErrors; import com.wordnik.swagger.annotations.ApiOperation; import com.wordnik.swagger.annotations.ApiParam; import org.jboss.resteasy.annotations.GZIP; import org.rhq.core.domain.common.EntityContext; import org.rhq.core.domain.criteria.MeasurementScheduleCriteria; import org.rhq.core.domain.measurement.DataType; import org.rhq.core.domain.measurement.MeasurementAggregate; import org.rhq.core.domain.measurement.MeasurementBaseline; import org.rhq.core.domain.measurement.MeasurementDataNumeric; import org.rhq.core.domain.measurement.MeasurementDataPK; import org.rhq.core.domain.measurement.MeasurementDataTrait; import org.rhq.core.domain.measurement.MeasurementDefinition; import org.rhq.core.domain.measurement.MeasurementSchedule; import org.rhq.core.domain.measurement.MeasurementScheduleRequest; import org.rhq.core.domain.measurement.calltime.CallTimeData; import org.rhq.core.domain.measurement.calltime.CallTimeDataComposite; import org.rhq.core.domain.measurement.composite.MeasurementDataNumericHighLowComposite; 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.util.PageControl; import org.rhq.core.domain.util.PageList; import org.rhq.enterprise.server.RHQConstants; import org.rhq.enterprise.server.measurement.CallTimeDataManagerLocal; import org.rhq.enterprise.server.measurement.MeasurementDataManagerLocal; import org.rhq.enterprise.server.measurement.MeasurementDefinitionManagerLocal; import org.rhq.enterprise.server.measurement.MeasurementScheduleManagerLocal; import org.rhq.enterprise.server.resource.ResourceManagerLocal; import org.rhq.enterprise.server.resource.group.ResourceGroupManagerLocal; import org.rhq.enterprise.server.rest.domain.Baseline; import org.rhq.enterprise.server.rest.domain.CallTimeValueRest; import org.rhq.enterprise.server.rest.domain.Datapoint; import org.rhq.enterprise.server.rest.domain.DoubleValue; import org.rhq.enterprise.server.rest.domain.Link; import org.rhq.enterprise.server.rest.domain.MetricAggregate; import org.rhq.enterprise.server.rest.domain.MetricDefinitionAggregate; import org.rhq.enterprise.server.rest.domain.MetricSchedule; import org.rhq.enterprise.server.rest.domain.NumericDataPoint; import org.rhq.enterprise.server.rest.domain.RHQErrorWrapper; import org.rhq.enterprise.server.rest.domain.StringValue; import org.rhq.enterprise.server.storage.StorageClientManager; import org.rhq.server.metrics.MetricsDAO; import org.rhq.server.metrics.domain.RawNumericMetric; /** * Deal with metrics * @author Heiko W. Rupp */ @Api(value = "Deal with metrics", description = "This part of the API deals with exporting and adding metrics") @Produces({"application/json","application/xml", "text/html"}) @Path("/metric") @Interceptors(SetCallerInterceptor.class) @Stateless public class MetricHandlerBean extends AbstractRestBean { static final String NO_RESOURCE_FOR_ID = "If no resource with the passed id exists"; static final String NO_SCHEDULE_FOR_ID = "No schedule with the passed id exists"; @EJB CallTimeDataManagerLocal calltimeDataManager; @EJB MeasurementDataManagerLocal dataManager; @EJB MeasurementScheduleManagerLocal scheduleManager; @EJB MeasurementDefinitionManagerLocal definitionManager; @EJB ResourceManagerLocal resMgr; @EJB ResourceGroupManagerLocal groupMgr; @EJB MetricHandlerBean metricHandlerBean; @EJB private StorageClientManager sessionManager; @PersistenceContext(unitName = RHQConstants.PERSISTENCE_UNIT_NAME) EntityManager em; private static final long EIGHT_HOURS = 8 * 3600L * 1000L; private static final long SEVEN_DAYS = 7L*86400*1000; @GZIP @GET @Path("data/{scheduleId}") @Produces({MediaType.APPLICATION_JSON,MediaType.APPLICATION_XML,MediaType.TEXT_HTML}) @ApiOperation(value = "Get the bucketized metric values for the schedule.") @ApiError(code = 404, reason = NO_SCHEDULE_FOR_ID) public Response getMetricData( @ApiParam("Schedule Id of the values to query") @PathParam("scheduleId") int scheduleId, @ApiParam(value = "Start time since epoch.", defaultValue = "End time - 8h") @QueryParam( "startTime") long startTime, @ApiParam(value = "End time since epoch.", defaultValue = "Now") @QueryParam("endTime") long endTime, @ApiParam("Number of buckets") @QueryParam("dataPoints") @DefaultValue("60") int dataPoints, @ApiParam(value = "Hide rows that are NaN only", defaultValue = "false") @QueryParam( "hideEmpty") boolean hideEmpty, @Context HttpHeaders headers) { if (dataPoints<=0) throw new BadArgumentException("dataPoints","must be >0"); if (startTime==0) { endTime = System.currentTimeMillis(); startTime = endTime - EIGHT_HOURS; } MediaType mediaType = headers.getAcceptableMediaTypes().get(0); boolean isHtml = mediaType.equals(MediaType.TEXT_HTML_TYPE); MeasurementSchedule schedule = obtainSchedule(scheduleId, false, DataType.MEASUREMENT); MeasurementAggregate aggr = dataManager.getMeasurementAggregate(caller, scheduleId, startTime, endTime); MetricAggregate res = new MetricAggregate(scheduleId, aggr.getMin(),aggr.getAvg(),aggr.getMax()); int definitionId = schedule.getDefinition().getId(); List<List<MeasurementDataNumericHighLowComposite>> listList = dataManager.findDataForResource(caller, schedule.getResource().getId(), new int[]{definitionId}, startTime, endTime, dataPoints); if (!listList.isEmpty()) { List<MeasurementDataNumericHighLowComposite> list = listList.get(0); fillInDatapoints(res, list, scheduleId, hideEmpty, isHtml); } CacheControl cc = new CacheControl(); int maxAge = (int) (schedule.getInterval() / 1000L)/2; // millis ; half of schedule interval cc.setMaxAge(maxAge); // these are seconds cc.setPrivate(false); cc.setNoCache(false); Response.ResponseBuilder builder; if (isHtml) { String htmlString = renderTemplate("metricData", res); builder = Response.ok(htmlString,mediaType); } else builder= Response.ok(res,mediaType); builder.cacheControl(cc); return builder.build(); } @GZIP @GET @Path("data/group/{groupId}/{definitionId}") @ApiOperation(value = "Get the bucketized metric values for the metric definition of the group ") @Produces({MediaType.APPLICATION_JSON,MediaType.APPLICATION_XML,MediaType.TEXT_HTML}) public Response getMetricDataForGroupAndDefinition( @ApiParam("Id of the group to query") @PathParam("groupId") int groupId, @ApiParam("Id of the metric definition to retrieve") @PathParam("definitionId") int definitionId, @ApiParam(value = "Start time since epoch.", defaultValue = "End time - 8h") @QueryParam( "startTime") long startTime, @ApiParam(value = "End time since epoch.", defaultValue = "Now") @QueryParam("endTime") long endTime, @ApiParam("Number of buckets") @QueryParam("dataPoints") @DefaultValue( "60") int dataPoints, @ApiParam(value = "Hide rows that are NaN only", defaultValue = "false") @QueryParam( "hideEmpty") boolean hideEmpty, @Context HttpHeaders headers) { if (startTime==0) { endTime = System.currentTimeMillis(); startTime = endTime - EIGHT_HOURS; } if (dataPoints<1) throw new BadArgumentException("dataPoints","must be >=0"); MediaType mediaType = headers.getAcceptableMediaTypes().get(0); boolean isHtml = mediaType.equals(MediaType.TEXT_HTML_TYPE); fetchGroup(groupId,true); // Make sure the group exists and is compatible MeasurementDefinition definition = definitionManager.getMeasurementDefinition(caller,definitionId); if (definition==null) { throw new StuffNotFoundException("There is no definition with id " + definitionId); } MeasurementAggregate aggr = dataManager.getAggregate(caller, groupId, definitionId, startTime, endTime); MetricAggregate res = new MetricAggregate(definitionId, aggr.getMin(),aggr.getAvg(),aggr.getMax()); res.setGroup(true); List<List<MeasurementDataNumericHighLowComposite>> listList = dataManager.findDataForCompatibleGroup(caller, groupId,definitionId,startTime,endTime,dataPoints); if (listList.isEmpty()) { throw new StuffNotFoundException("Data for group with id " + groupId + " and definition " + definitionId); } List<MeasurementDataNumericHighLowComposite> list = listList.get(0); if (!listList.isEmpty()) { fillInDatapoints(res, list, definitionId, hideEmpty, isHtml); } CacheControl cc = new CacheControl(); int maxAge = (int) (definition.getDefaultInterval() / 1000L)/2; // millis ; half of schedule interval cc.setMaxAge(maxAge); // these are seconds cc.setPrivate(false); cc.setNoCache(false); Response.ResponseBuilder builder; if (isHtml) { String htmlString = renderTemplate("metricData", res); builder = Response.ok(htmlString,mediaType); } else builder= Response.ok(res,mediaType); builder.cacheControl(cc); return builder.build(); } /** * Get the schedule for the passed schedule id * * @param scheduleId id to look up * @param force If true always go to the DB, otherwise search in the cache first * @param type Type of schedule to look for * @return schedule * @throws StuffNotFoundException if there is no schedule with the passed id */ private MeasurementSchedule obtainSchedule(int scheduleId, boolean force, DataType type) { MeasurementSchedule schedule=null; if(!force) schedule = getFromCache(scheduleId,MeasurementSchedule.class); if (schedule==null) { schedule = scheduleManager.getScheduleById(caller,scheduleId); if (schedule==null) { throw new StuffNotFoundException("Schedule with id " + scheduleId); } else putToCache(scheduleId,MeasurementSchedule.class,schedule); } if (schedule.getDefinition().getDataType()!= type) throw new BadArgumentException("Schedule [" + scheduleId + "]","it is not a ("+ type + ") metric"); return schedule; } private MetricAggregate fillInDatapoints(MetricAggregate res, List<MeasurementDataNumericHighLowComposite> list, int scheduleId, boolean hideEmpty, boolean isHtmlOutput) { long minTime=Long.MAX_VALUE; long maxTime=0; res.setScheduleId(scheduleId); for (MeasurementDataNumericHighLowComposite c : list) { long timestamp = c.getTimestamp(); if (Double.isNaN(c.getValue()) && hideEmpty) continue; MetricAggregate.DataPoint dp; if (isHtmlOutput) { dp = new MetricAggregate.DataPoint(timestamp); Double v = c.getLowValue(); v= nullifyIfNaN(v); dp.setLow(v); v = c.getHighValue(); v =nullifyIfNaN(v); dp.setHigh(v); v = c.getValue(); v= nullifyIfNaN(v); dp.setValue(v); } else { dp = new MetricAggregate.DataPoint(timestamp,c.getValue(),c.getHighValue(),c.getLowValue()); } res.addDataPoint(dp); if (timestamp <minTime) minTime= timestamp; if (timestamp >maxTime) maxTime= timestamp; } res.setNumDataPoints(list.size()); res.setMaxTimeStamp(maxTime); res.setMinTimeStamp(minTime); return res; } private Double nullifyIfNaN(Double v) { if (Double.isNaN(v)) v =null; return v; } @GZIP @GET @Path("data") @Produces({MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML, MediaType.TEXT_HTML}) @ApiOperation(value = "Return bucketized metric data (60 points) for the passed schedules") @ApiErrors({ @ApiError(code = 404, reason = NO_SCHEDULE_FOR_ID), @ApiError(code = 406, reason = "No schedules requested"), @ApiError(code = 406, reason = "Schedule Ids are not numeric") }) public Response getMetricDataMulti( @ApiParam(value = "A comma separated list of schedule ids",required = true) @QueryParam("sid") String schedules, @ApiParam("Start time in ms since epoch. Default is now -8h") @QueryParam("startTime") long startTime, @ApiParam("End time in ms since epoch. Default is now") @QueryParam("endTime") long endTime, @ApiParam("Number of buckets") @QueryParam("dataPoints") @DefaultValue( "60") int dataPoints, @ApiParam("Should empty datapoints be hidden") @DefaultValue("false") @QueryParam("hideEmpty") boolean hideEmpty, @Context HttpHeaders headers) { if (dataPoints<=0) throw new BadArgumentException("dataPoints","must be >0"); MediaType mediaType = headers.getAcceptableMediaTypes().get(0); long now = System.currentTimeMillis(); if (endTime==0) endTime = now; if (startTime==0) { endTime = System.currentTimeMillis(); startTime = endTime - EIGHT_HOURS; } if (schedules==null) { throw new ParameterMissingException("sid"); } String[] tmp = schedules.split(","); Integer[] scheduleIds = new Integer[tmp.length]; try { for (int i = 0; i < tmp.length ; i++) scheduleIds[i] = Integer.parseInt(tmp[i]); } catch (NumberFormatException nfe) { throw new BadArgumentException("Sid" , nfe.getMessage()); } List<MetricAggregate> resList = new ArrayList<MetricAggregate>(scheduleIds.length); for (Integer scheduleId : scheduleIds) { MeasurementSchedule sched = scheduleManager.getScheduleById(caller,scheduleId); if (sched==null) throw new StuffNotFoundException("Schedule with id " + scheduleId); int definitionId = sched.getDefinition().getId(); List<List<MeasurementDataNumericHighLowComposite>> listList = dataManager.findDataForContext(caller, EntityContext.forResource(sched.getResource().getId()),definitionId,startTime,endTime,dataPoints); if (!listList.isEmpty()) { MeasurementAggregate measurementAggregate = dataManager.getMeasurementAggregate(caller,scheduleId,startTime,endTime); List<MeasurementDataNumericHighLowComposite> list = listList.get(0); MetricAggregate res = new MetricAggregate(scheduleId,measurementAggregate.getMin(),measurementAggregate.getAvg(),measurementAggregate.getMax()); boolean isHtml = mediaType.equals(MediaType.TEXT_HTML_TYPE); res = fillInDatapoints(res, list, scheduleId, hideEmpty, isHtml); resList.add(res); } else throw new StuffNotFoundException("Metrics for schedule " + scheduleId); } GenericEntity<List<MetricAggregate>> metAgg = new GenericEntity<List<MetricAggregate>>(resList) {}; return Response.ok(metAgg,mediaType).build(); } /** * Return a metric schedule with the respective status codes for cache validation * * @param scheduleId ID of the schedule * @param request the REST request - injected by the REST framework * @param headers the REST request http headers - injected by the REST framework * @param uriInfo info about the called uri to build links * @return Schedule with respective headers */ @GET @Path("/schedule/{id}") @Produces({MediaType.APPLICATION_JSON,MediaType.APPLICATION_XML,MediaType.TEXT_HTML}) @ApiOperation("Get the metric schedule for the passed id") @ApiError(code = 404, reason = NO_SCHEDULE_FOR_ID) public Response getSchedule(@ApiParam("Schedule Id") @PathParam("id") int scheduleId, @Context Request request, @Context HttpHeaders headers, @Context UriInfo uriInfo) { MediaType mediaType = headers.getAcceptableMediaTypes().get(0); MeasurementSchedule schedule; Response.ResponseBuilder builder; // Create a cache control CacheControl cc = new CacheControl(); cc.setMaxAge(300); // Schedules are valid for 5 mins cc.setPrivate(false); // Proxies may cache this schedule = getFromCache(scheduleId, MeasurementSchedule.class); if (schedule!=null) { // If it is on cache, quickly return if match long tim = schedule.getMtime() != null ? schedule.getMtime() : 0; EntityTag eTag = new EntityTag(Long.toOctalString(schedule.hashCode()+tim)); // factor in mtime in etag builder = request.evaluatePreconditions(new Date(tim),eTag); if (builder!=null) { builder.cacheControl(cc); return builder.build(); } } if (schedule==null) { schedule = scheduleManager.getScheduleById(caller, scheduleId); if (schedule==null) throw new StuffNotFoundException("Schedule with id " + scheduleId); else putToCache(scheduleId, MeasurementSchedule.class, schedule); } MeasurementDefinition definition = schedule.getDefinition(); // MetricSchedule metricSchedule = getMetricScheduleInternal(uriInfo,schedule,definition); todo MetricSchedule metricSchedule = new MetricSchedule(schedule.getId(), definition.getName(), definition.getDisplayName(), schedule.isEnabled(), schedule.getInterval(), definition.getUnits().toString(), definition.getDataType().toString()); metricSchedule.setDefinitionId(definition.getId()); if (schedule.getMtime()!=null) metricSchedule.setMtime(schedule.getMtime()); // Check for conditional get again // Interestingly computing the hashCode of the original schedule is slower, as it also // pulls in data from the definition and the resource long tim = schedule.getMtime() != null ? schedule.getMtime() : 0; EntityTag eTag = new EntityTag(Long.toOctalString(schedule.hashCode()+tim)); builder = request.evaluatePreconditions(new Date(tim),eTag); // factor in mtime in etag if (builder==null) { // preconditions not met, we need to send the resource UriBuilder uriBuilder; URI uri; Link link; if (definition.getDataType()==DataType.MEASUREMENT) { // create link to metrics uriBuilder = uriInfo.getBaseUriBuilder(); uriBuilder.path("metric/data/" + scheduleId); uri = uriBuilder.build(); link = new Link("metric",uri.toString()); metricSchedule.addLink(link); } // create link to the resource uriBuilder = uriInfo.getBaseUriBuilder(); uriBuilder.path("resource/" + schedule.getResource().getId()); uri = uriBuilder.build(); link = new Link("resource",uri.toString()); metricSchedule.addLink(link); // Link for updates uriBuilder = uriInfo.getAbsolutePathBuilder(); uri = uriBuilder.build(); Link updateLink = new Link("edit",uri.toString()); metricSchedule.addLink(updateLink); updateLink = new Link("self",uri.toString()); metricSchedule.addLink(updateLink); metricSchedule.addLink(createUILink(uriInfo, UILinkTemplate.METRIC_SCHEDULE, schedule.getResource().getId())); if (mediaType.equals(MediaType.TEXT_HTML_TYPE)) { builder = Response.ok(renderTemplate("metricSchedule", metricSchedule), mediaType); } else { builder = Response.ok(metricSchedule,mediaType); } } builder.cacheControl(cc); builder.tag(eTag); return builder.build(); } @GZIP @GET @Path("data/resource/{resourceId}") @ApiOperation("Retrieve a list of high/low/average/data aggregates for the resource") @ApiError(code = 404, reason = NO_RESOURCE_FOR_ID) public List<MetricAggregate> getAggregatesForResource( @ApiParam("Id of the resource to query") @PathParam("resourceId") int resourceId, @ApiParam(value = "Start time since epoch.", defaultValue = "End time - 8h") @QueryParam( "startTime") long startTime, @ApiParam(value = "End time since epoch.", defaultValue = "Now") @QueryParam("endTime") long endTime, @ApiParam(value = "Include data points") @DefaultValue("false") @QueryParam("includeDataPoints") boolean includeDataPoints, @ApiParam("Number of buckets (if include data points))") @QueryParam("dataPoints") @DefaultValue( "60") int dataPoints, @ApiParam(value = "Hide rows that are NaN only", defaultValue = "false") @QueryParam( "hideEmpty") boolean hideEmpty) { long now = System.currentTimeMillis(); if (endTime==0) endTime = now; if (startTime==0) { startTime = endTime - EIGHT_HOURS; } fetchResource(resourceId); // Check if the resource exists at all List<MeasurementSchedule> schedules = scheduleManager.findSchedulesForResourceAndType(caller, resourceId, DataType.MEASUREMENT, null,false); for (MeasurementSchedule sched: schedules) { putToCache(sched.getId(),MeasurementSchedule.class,sched); } List<MetricAggregate> ret = new ArrayList<MetricAggregate>(schedules.size()); for (MeasurementSchedule schedule: schedules) { MeasurementAggregate aggr = dataManager.getMeasurementAggregate(caller,schedule.getId(),startTime,endTime); MetricAggregate res = new MetricAggregate(schedule.getId(), aggr.getMin(),aggr.getAvg(),aggr.getMax()); if (includeDataPoints) { int definitionId = schedule.getDefinition().getId(); List<List<MeasurementDataNumericHighLowComposite>> listList = dataManager.findDataForResource(caller, schedule.getResource().getId(), new int[]{definitionId}, startTime, endTime, dataPoints); if (!listList.isEmpty()) { List<MeasurementDataNumericHighLowComposite> list = listList.get(0); fillInDatapoints(res, list, schedule.getId(), hideEmpty, false); } } ret.add(res); } return ret; } @GZIP @GET @Path("data/group/{groupId}") @ApiOperation("Retrieve a list of high/low/average/data aggregates for the group") @ApiError(code = 404, reason = "There is no group with the passed id") public List<MetricDefinitionAggregate> getAggregatesForGroup( @ApiParam("Id of the group to query") @PathParam("groupId") int groupId, @ApiParam(value = "Start time since epoch.", defaultValue="End time - 8h") @QueryParam("startTime") long startTime, @ApiParam(value = "End time since epoch.", defaultValue = "Now") @QueryParam("endTime") long endTime) { long now = System.currentTimeMillis(); if (endTime==0) endTime = now; if (startTime==0) { startTime = endTime - EIGHT_HOURS; } ResourceGroup group = fetchGroup(groupId, true); Set<MeasurementDefinition> definitions = group.getResourceType().getMetricDefinitions(); List<MetricDefinitionAggregate> ret = new ArrayList<MetricDefinitionAggregate>(definitions.size()); for (MeasurementDefinition def : definitions) { if (def.getDataType()==DataType.MEASUREMENT) { MeasurementAggregate aggregate = dataManager.getAggregate(caller, groupId, def.getId(), startTime, endTime); MetricDefinitionAggregate res = new MetricDefinitionAggregate(def.getId(), aggregate.getMin(),aggregate.getAvg(),aggregate.getMax()); ret.add(res); } } return ret; } @PUT @Path("/schedule/{id}") @Consumes({MediaType.APPLICATION_JSON,MediaType.APPLICATION_XML}) @Produces({MediaType.APPLICATION_JSON,MediaType.APPLICATION_XML}) @ApiOperation(value = "Update the schedule (enabled, interval) ", responseClass = "MetricSchedule") @ApiError(code = 404, reason = NO_SCHEDULE_FOR_ID) public Response updateSchedule(@ApiParam("Id of the schedule to update") @PathParam("id") int scheduleId, @ApiParam(value = "New schedule data", required = true) MetricSchedule in, @Context HttpHeaders headers) { MeasurementSchedule schedule = scheduleManager.getScheduleById(caller, scheduleId); if (schedule==null) throw new StuffNotFoundException("Schedule with id " + scheduleId); schedule.setEnabled(in.getEnabled()); schedule.setInterval(in.getCollectionInterval()); scheduleManager.updateSchedule(caller, schedule); schedule = scheduleManager.getScheduleById(caller,scheduleId); putToCache(scheduleId, MeasurementSchedule.class, schedule); MeasurementDefinition def = schedule.getDefinition(); MetricSchedule ret = new MetricSchedule(scheduleId,def.getName(),def.getDisplayName(), schedule.isEnabled(),schedule.getInterval(),def.getUnits().toString(),def.getDataType().toString()); ret.setDefinitionId(def.getId()); return Response.ok(ret,headers.getAcceptableMediaTypes().get(0)).build(); } @GET @Path("/definition/{id}") @Produces({MediaType.APPLICATION_JSON,MediaType.APPLICATION_XML}) @ApiOperation(value = "Get the definition ", responseClass = "MetricSchedule") @ApiError(code = 404, reason = "No definition exists for the given id.") public Response getDefinition(@ApiParam("Id of the definition to obtain") @PathParam("id") int definitionId, @Context HttpHeaders headers) { MeasurementDefinition measurementDefinition = definitionManager.getMeasurementDefinition(caller, definitionId); if (measurementDefinition==null) throw new StuffNotFoundException("Definition with id " + definitionId); MetricSchedule schedule = new MetricSchedule(definitionId, measurementDefinition.getName(), measurementDefinition.getDisplayName(), measurementDefinition.isDefaultOn(), measurementDefinition.getDefaultInterval(), measurementDefinition.getUnits().getName(), measurementDefinition.getDataType().name()); return Response.ok(schedule,headers.getAcceptableMediaTypes().get(0)).build(); } @PUT @Path("/definition/{id}") @Consumes({MediaType.APPLICATION_JSON,MediaType.APPLICATION_XML}) @Produces({MediaType.APPLICATION_JSON,MediaType.APPLICATION_XML}) @ApiOperation(value = "Update the definition (default enabled, default interval)", notes = "This operation may internally take a long time to complete and is thus only triggered by this call. " + "A return code of 200 only indicates that the operation was successfully submitted." , responseClass = "MetricSchedule") @ApiError(code = 404, reason = "No definition exists for the given id.") public Response updateDefinition(@ApiParam("Id of the definition to update") @PathParam("id") int definitionId, @ApiParam(value = "New definition data", required = true) MetricSchedule in, @ApiParam(value = "Update existing schedules for this definition as well?") @QueryParam("updateExisting") @DefaultValue("false") boolean updateExisting, @Context HttpHeaders headers) { MeasurementDefinition measurementDefinition = definitionManager.getMeasurementDefinition(caller, definitionId); if (measurementDefinition==null) throw new StuffNotFoundException("Definition with id " + definitionId); // Call an async method to do the work as this can take a looong time metricHandlerBean.submitDefinitionChange(definitionId, in, updateExisting); StringValue ret = new StringValue("Request submitted - this may take a while to complete."); return Response.ok(ret,headers.getAcceptableMediaTypes().get(0)).build(); } /** * This method does the real updating, it is not exposed to the REST-clients, but must be public so that #updateDefinition can * call it and the container does an asynchronous request. * @param definitionId Id of the measeuremnt definition to update * @param in The data to be put in * @param updateExisting Should existing schedules of the metric als be updated? */ @Asynchronous public void submitDefinitionChange(int definitionId, MetricSchedule in, boolean updateExisting) { scheduleManager.updateDefaultCollectionIntervalAndEnablementForMeasurementDefinitions(caller,new int[]{definitionId},in.getCollectionInterval(),in.getEnabled(),updateExisting); } @GZIP @ApiOperation(value = "Expose the raw metrics of a single schedule. This can only expose raw data, which means the start date may " + "not be older than 7 days.") @GET @Path("data/{scheduleId}/raw") @Produces({MediaType.APPLICATION_JSON,MediaType.APPLICATION_XML,"text/csv",MediaType.TEXT_HTML}) @ApiErrors({ @ApiError(code = 404, reason = NO_SCHEDULE_FOR_ID) }) public StreamingOutput getMetricDataRaw(@ApiParam(required = true) @PathParam("scheduleId") int scheduleId, @ApiParam(value = "Start time since epoch", defaultValue = "Now - 8h") @QueryParam("startTime") long startTime, @ApiParam(value = "End time since epoch", defaultValue = "Now") @QueryParam( "endTime") long endTime, @ApiParam(defaultValue = "8h = 28800000ms", value = "Timespan in ms") @QueryParam("duration") long duration, @Context HttpHeaders headers) { MediaType mediaType = headers.getAcceptableMediaTypes().get(0); long now = System.currentTimeMillis(); if (endTime==0) endTime = now; if (startTime==0) startTime = endTime - EIGHT_HOURS; if (duration>0) // overrides start time startTime = endTime - duration*1000L; // duration is in seconds if (startTime < now - SEVEN_DAYS) throw new IllegalArgumentException("(Computed) start time is older than 7 days"); if (startTime == endTime) { endTime++; // add 1ms, as otherwise the backend fails to find a value at the startTime } // Check if the schedule exists obtainSchedule(scheduleId, false, DataType.MEASUREMENT); RawNumericStreamingOutput so = new RawNumericStreamingOutput(); so.scheduleId = scheduleId; so.startTime = startTime; so.endTime = endTime; so.mediaType = mediaType; return so; } @GET @ApiOperation("Expose callTime data for given scheduleId") @ApiErrors({ @ApiError(code = 404, reason = NO_SCHEDULE_FOR_ID) }) @Produces({MediaType.APPLICATION_JSON,MediaType.TEXT_HTML,MediaType.APPLICATION_XML}) @Path("data/{scheduleId}/callTime") public StreamingOutput getCallTimesForResource( @ApiParam(required = true) @PathParam("scheduleId") int scheduleId, @ApiParam(value = "Start time since epoch", defaultValue = "Now - 8h") @QueryParam("startTime") long startTime, @ApiParam(value = "End time since epoch", defaultValue = "Now") @QueryParam("endTime") long endTime, @ApiParam(value = "True to return callTimes aggregated by callDestination", defaultValue = "True") @DefaultValue("true") @QueryParam("aggregate") boolean aggregate, @Context HttpHeaders headers ) { MediaType mediaType = headers.getAcceptableMediaTypes().get(0); MeasurementSchedule schedule = obtainSchedule(scheduleId, false, DataType.CALLTIME); long now = System.currentTimeMillis(); if (endTime==0) endTime = now; if (startTime==0) startTime = endTime - EIGHT_HOURS; if (startTime == endTime) { endTime++; // add 1ms, as otherwise the backend fails to find a value at the startTime } PageList<CallTimeDataComposite> callTimes; if (aggregate) { callTimes = calltimeDataManager.findCallTimeDataForResource(caller, schedule.getId(), startTime, endTime, PageControl.getUnlimitedInstance()); } else { callTimes = calltimeDataManager.findCallTimeDataRawForResource(caller, schedule.getId(), startTime, endTime, PageControl.getUnlimitedInstance()); } return new CallTimeDataStreamingOutput(mediaType,callTimes); } @PUT @Produces({MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML}) @Consumes({MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML}) @ApiOperation("Submit a callTime metrics to the server") @ApiErrors({ @ApiError(code=404, reason = NO_SCHEDULE_FOR_ID), @ApiError(code=406, reason = "beginTime is older than 7 days"), @ApiError(code=406, reason = "callDestination is null"), @ApiError(code=406, reason = "duration is negative number") }) @Path("data/{scheduleId}/callTime") public Response putCallTimeValues( @ApiParam("Id of the schedule") @PathParam("scheduleId") int scheduleId, List<CallTimeValueRest> callTimes, @Context HttpHeaders headers, @Context UriInfo uriInfo ) { if (callTimes.size() < 1) { return Response.ok().build(); } MediaType mediaType = headers.getAcceptableMediaTypes().get(0); MeasurementSchedule schedule = obtainSchedule(scheduleId, false, DataType.CALLTIME); MeasurementScheduleRequest req = new MeasurementScheduleRequest(schedule); CallTimeData ctd = new CallTimeData(req); long now = System.currentTimeMillis(); long startTime = System.currentTimeMillis(); long endTime = startTime - SEVEN_DAYS; int idx=0; for (CallTimeValueRest v : callTimes) { // validate if (v.getCallDestination() == null) { throw new IllegalArgumentException("Invalid item["+idx+"] "+v+" : callDestination must not be null"); } if (v.getDuration()<0) { throw new IllegalArgumentException("Invalid item["+idx+"] "+v+" : duration must be a positive number"); } if (now - SEVEN_DAYS > v.getBeginTime()) { throw new IllegalArgumentException("Invalid item["+idx+"] "+v+" : beginTime is older than 7 days"); } // measure interval, so we can return proper location header if (v.getBeginTime() < startTime) { startTime = v.getBeginTime(); } if (v.getBeginTime()+v.getDuration() > endTime) { endTime = v.getBeginTime()+v.getDuration(); } ctd.addCallData(v.getCallDestination(), new Date(v.getBeginTime()), v.getDuration()); idx++; } Set<CallTimeData> data = new HashSet<CallTimeData>(); data.add(ctd); calltimeDataManager.addCallTimeData(data); UriBuilder uriBuilder = uriInfo.getBaseUriBuilder(); uriBuilder.path("/metric/data/{scheduleId}/callTime"); uriBuilder.queryParam("startTime",startTime); uriBuilder.queryParam("endTime",endTime); URI uri = uriBuilder.build(scheduleId); return Response.created(uri).type(mediaType).build(); } @PUT @Produces({MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML}) @Consumes({MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML}) @ApiOperation("Submit a single (numerical) metric to the server") @ApiErrors({ @ApiError(code=404, reason = NO_SCHEDULE_FOR_ID), @ApiError(code=406, reason = "Timestamp is older than 7 days") }) @Path("data/{scheduleId}/raw/{timeStamp}") public Response putMetricValue(@ApiParam("Id of the schedule") @PathParam("scheduleId") int scheduleId, @ApiParam("Timestamp of the metric") @PathParam("timeStamp") long timestamp, @ApiParam(value = "Data value", required = true) DoubleValue value, @Context HttpHeaders headers, @Context UriInfo uriInfo) { MediaType mediaType = headers.getAcceptableMediaTypes().get(0); obtainSchedule(scheduleId, false, DataType.MEASUREMENT); long now = System.currentTimeMillis(); if (timestamp < now - SEVEN_DAYS) throw new IllegalArgumentException("Timestamp is older than 7 days"); Set<MeasurementDataNumeric> data = new HashSet<MeasurementDataNumeric>(1); data.add(new MeasurementDataNumeric(timestamp,scheduleId,value.getValue())); dataManager.addNumericData(data); UriBuilder uriBuilder = uriInfo.getBaseUriBuilder(); uriBuilder.path("/metric/data/{scheduleId}/raw"); uriBuilder.queryParam("startTime",timestamp); uriBuilder.queryParam("endTime",timestamp); URI uri = uriBuilder.build(scheduleId); return Response.created(uri).type(mediaType).build(); } @PUT @Path("data/{scheduleId}/trait/{timeStamp}") @Consumes({MediaType.APPLICATION_JSON,MediaType.APPLICATION_XML}) @ApiOperation(value = "Submit a new trait value for the passed schedule id") @ApiErrors({ @ApiError(code=404, reason = NO_SCHEDULE_FOR_ID), @ApiError(code=406, reason = "Timestamp is older than 7 days") }) public Response putTraitValue(@ApiParam("Id of the schedule") @PathParam("scheduleId") int scheduleId, @ApiParam("Timestamp of the metric") @PathParam("timeStamp") long timestamp, @ApiParam(value = "Data value", required = true) StringValue value) { obtainSchedule(scheduleId, false, DataType.TRAIT); long now = System.currentTimeMillis(); if (timestamp < now - SEVEN_DAYS) throw new IllegalArgumentException("Timestamp is older than 7 days"); Set<MeasurementDataTrait> traits = new HashSet<MeasurementDataTrait>(1); MeasurementDataPK pk = new MeasurementDataPK(timestamp,scheduleId); traits.add(new MeasurementDataTrait(pk,value.getValue())); dataManager.addTraitData(traits); return Response.ok().build(); } @GET @Path("data/{scheduleId}/trait") @Produces({MediaType.APPLICATION_JSON,MediaType.APPLICATION_XML}) @ApiOperation(value="Get the current value of the trait with the passed schedule id", responseClass = "StringValue") @ApiError(code = 404, reason = NO_SCHEDULE_FOR_ID) public Response getTraitValue(@ApiParam("Id of the schedule") @PathParam("scheduleId") int scheduleId) { MeasurementSchedule schedule = obtainSchedule(scheduleId, false, DataType.TRAIT); List<MeasurementDataTrait> traits = dataManager.findTraits(caller,schedule.getResource().getId(),schedule.getDefinition().getId()); Response.ResponseBuilder builder; if (traits!=null && traits.size()>0) { builder = Response.ok(); StringValue value = new StringValue(traits.get(0).getValue()); builder.entity(value); } else { builder = Response.status(Response.Status.NOT_FOUND); } return builder.build(); } @POST @Path("data/raw") @Consumes({MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML}) @ApiOperation(value="Submit a series of (numerical) metric values to the server",responseClass = "No response") @ApiErrors({ @ApiError(code = 201, reason = "There are some submitted datapoints with non-existing scheduleId, API returns rejected values back to client, valid values are accepted"), @ApiError(code = 403, reason = "All submitted datapoints have non-existing scheduleId, API returns rejected values back to client") }) public Response postMetricValues(Collection<NumericDataPoint> points, @Context HttpHeaders headers) { MediaType mediaType = headers.getAcceptableMediaTypes().get(0); Set<MeasurementDataNumeric> data = new HashSet<MeasurementDataNumeric>(points.size()); List<NumericDataPoint> rejected = new ArrayList<NumericDataPoint>(); for (NumericDataPoint point : points) { if (isScheduleAccessible(point.getScheduleId())) { data.add(new MeasurementDataNumeric(point.getTimeStamp(), point.getScheduleId(), point.getValue())); } else { rejected.add(point); } } if (rejected.isEmpty()) { dataManager.addNumericData(data); return Response.noContent().type(mediaType).build(); } else { Map<String, Object> resp = new HashMap<String, Object>(); resp.put("rejected", rejected); resp.put("message", "Schedules for rejected datapoints do not exist"); return Response.status(data.isEmpty() ? Status.FORBIDDEN : Status.CREATED).entity(resp).build(); } } private boolean isScheduleAccessible(int scheduleId) { // our Key is hopefully unique combination of scheduleId and caller (user) ID // it cannot be just scheduleId, because different users can have access different schedules CacheKey key = new CacheKey("existsScheduleForCaller", (31 * (1 + scheduleId) * 31 * (1 + caller.getId()))); Boolean accessible = (Boolean) cache.get(key); if (accessible != null) { return accessible.booleanValue(); } else { MeasurementScheduleCriteria criteria = new MeasurementScheduleCriteria(); criteria.addFilterId(scheduleId); PageList<MeasurementSchedule> schedules = scheduleManager.findSchedulesByCriteria(caller, criteria); if (schedules.isEmpty()) { cache.put(key, Boolean.FALSE); return false; } cache.put(key, Boolean.TRUE); return true; } } @POST @Path("data/raw/{resourceId}") @Consumes({MediaType.APPLICATION_JSON}) @ApiOperation(value="Submit a series of (numerical) metric values for a single resource to the server",responseClass = "No response") @ApiError(code = 403, reason = "Any metric from recieved dataPoints does not exist for given resource") public Response postMetricValues2(@PathParam("resourceId") int resourceId, Collection<Datapoint> points, @Context HttpHeaders headers) { MediaType mediaType = headers.getAcceptableMediaTypes().get(0); Set<MeasurementDataNumeric> data = new HashSet<MeasurementDataNumeric>(points.size()); for (Datapoint point : points) { Integer scheduleId = findScheduleId(resourceId, point.getMetric()); if (scheduleId != null) { data.add(new MeasurementDataNumeric(point.getTimestamp(), scheduleId,point.getValue())); } else { return Response .status(Status.FORBIDDEN) .entity( new RHQErrorWrapper("Metric name=" + point.getMetric() + " for resourceId=" + resourceId + " does not exist")).build(); } } dataManager.addNumericData(data); return Response.noContent().type(mediaType).build(); } private Integer findScheduleId(int resourceId, String metric) { CacheKey key = new CacheKey("schedulesForResource",resourceId); @SuppressWarnings("unchecked") Map<String,Integer> schedulesForResource = (Map<String, Integer>) cache.get(key); if (schedulesForResource!=null && schedulesForResource.containsKey(metric)) { return schedulesForResource.get(metric); } else { Resource res = fetchResource(resourceId); ResourceType resourceType = res.getResourceType(); int[] definitionIds = new int[resourceType.getMetricDefinitions().size()]; int i = 0; for (MeasurementDefinition def : resourceType.getMetricDefinitions()) { definitionIds[i]=def.getId(); i++; } List<MeasurementSchedule> schedules = scheduleManager.findSchedulesByResourceIdAndDefinitionIds(caller,resourceId,definitionIds); schedulesForResource = new HashMap<String, Integer>(schedules.size()); for (MeasurementSchedule schedule : schedules) { schedulesForResource.put(schedule.getDefinition().getName(),schedule.getId()); } cache.put(key,schedulesForResource); return schedulesForResource.get(metric); } } @GET @Path("data/{scheduleId}/baseline") @Produces({MediaType.APPLICATION_JSON,MediaType.APPLICATION_XML}) @ApiOperation(value = "Get the current baseline for the schedule") @ApiError(code = 404, reason = NO_SCHEDULE_FOR_ID) public Baseline getBaseline(@ApiParam("Id of the schedule") @PathParam("scheduleId") int scheduleId) { MeasurementSchedule schedule = obtainSchedule(scheduleId, true, DataType.MEASUREMENT); MeasurementBaseline mBase = schedule.getBaseline(); Baseline b; if (mBase==null) throw new StuffNotFoundException("Baseline for schedule [" + scheduleId +"]"); else b = new Baseline(mBase.getMin(),mBase.getMax(),mBase.getMean(),mBase.getComputeTime().getTime()); return b; } @PUT @Path("data/{scheduleId}/baseline") @Consumes({MediaType.APPLICATION_JSON,MediaType.APPLICATION_XML}) @ApiOperation(value = "Set a new baseline for the schedule") @ApiErrors({ @ApiError(code = 404, reason = NO_SCHEDULE_FOR_ID), @ApiError(code = 406 ,reason = "Baseline data is incorrect") }) public Response setBaseline(@ApiParam("Id of the schedule") @PathParam("scheduleId") int scheduleId, Baseline baseline, @Context UriInfo uriInfo) { MeasurementSchedule schedule = obtainSchedule(scheduleId, false, DataType.MEASUREMENT); // little bit of sanity checking if (baseline.getMin()>baseline.getMean() || baseline.getMean()>baseline.getMax() || baseline.getMin()>baseline.getMax()) { Response.ResponseBuilder builder = Response.status(Response.Status.NOT_ACCEPTABLE); builder.entity("Baseline not correct. it should be min<=mean<=max"); return builder.build(); } MeasurementBaseline mBase = schedule.getBaseline(); if (mBase == null) { mBase = new MeasurementBaseline(); mBase.setSchedule(schedule); schedule.setBaseline(mBase); em.persist(mBase); } mBase.setMax(baseline.getMax()); mBase.setMin(baseline.getMin()); mBase.setMean(baseline.getMean()); mBase.setUserEntered(true); scheduleManager.updateSchedule(caller,schedule); return Response.created(uriInfo.getRequestUriBuilder().build()).build(); } private class CallTimeDataStreamingOutput implements StreamingOutput { private final PageList<CallTimeDataComposite> callTimes; private final MediaType mediaType; public CallTimeDataStreamingOutput(MediaType mType, PageList<CallTimeDataComposite> callTimes) { this.callTimes = callTimes; this.mediaType= mType; } private void jsonOutput(CallTimeDataComposite c, PrintWriter pw) { pw.print("{"); pw.print("\"callDestination\":\""); pw.print(c.getCallDestination()); pw.print("\",\"minimum\":"); pw.print(c.getMinimum()); pw.print(",\"maximum\":"); pw.print(c.getMaximum()); pw.print(",\"average\":"); pw.print(c.getAverage()); pw.print(",\"total\":"); pw.print(c.getTotal()); pw.print(",\"count\":"); pw.print(c.getCount()); pw.print("}"); pw.flush(); } private void htmlOutput(CallTimeDataComposite c, PrintWriter pw) { pw.print("<tr>"); pw.print("<td>"); pw.print(c.getCallDestination()); pw.print("</td><td>"); pw.print(c.getMinimum()); pw.print("</td><td>"); pw.print(c.getMaximum()); pw.print("</td><td>"); pw.print(c.getAverage()); pw.print("</td><td>"); pw.print(c.getTotal()); pw.print("</td><td>"); pw.print(c.getCount()); pw.print("</td></tr>\n"); pw.flush(); } private void xmlOutput(CallTimeDataComposite c, PrintWriter pw) { pw.print(" <callTime "); pw.print("callDestination=\""); pw.print(c.getCallDestination()); pw.print("\" minimum=\""); pw.print(c.getMinimum()); pw.print("\" maximum=\""); pw.print(c.getMaximum()); pw.print("\" average=\""); pw.print(c.getAverage()); pw.print("\" total=\""); pw.print(c.getTotal()); pw.print("\" count=\""); pw.print(c.getCount()); pw.print("\" />\n"); pw.flush(); } @Override public void write(OutputStream os) throws IOException, WebApplicationException { PrintWriter pw = new PrintWriter(os); Iterator<CallTimeDataComposite> i = callTimes.iterator(); if (mediaType.equals(MediaType.APPLICATION_JSON_TYPE)) { pw.print("["); if (i.hasNext()) { jsonOutput(i.next(), pw); } while (i.hasNext()) { pw.print(",\n"); jsonOutput(i.next(), pw); } pw.println("]"); } else if (mediaType.equals(MediaType.TEXT_HTML_TYPE)) { pw.println("<table>"); pw.print("<tr><th>callDestination</th><th>minimum</th><th>maximum</th><th>average</th><th>total</th><th>count</th></tr>\n"); while (i.hasNext()) { htmlOutput(i.next(), pw); } pw.println("</table>"); } else if (mediaType.equals(MediaType.APPLICATION_XML_TYPE)) { pw.println("<collection>\n"); while (i.hasNext()) { xmlOutput(i.next(), pw); } pw.println("</collection>"); } else { //default pw.print("["); if (i.hasNext()) { jsonOutput(i.next(), pw); } while (i.hasNext()) { pw.print(",\n"); jsonOutput(i.next(), pw); } pw.println("]"); } pw.flush(); pw.close(); } } /** * Write the numeric data points to the output stream in the encoding * requested from the mediaType without creating tons of objects in the * middle to have them marshalled by JAX-RS */ private class RawNumericStreamingOutput implements StreamingOutput { int scheduleId; long startTime; long endTime; MediaType mediaType; private void jsonOutput(PrintWriter pw, Iterable<RawNumericMetric> resultSet) { boolean needsComma = false; pw.println("["); for (RawNumericMetric metric : resultSet) { if (needsComma) { pw.print(",\n"); } needsComma = true; pw.print("{"); pw.print("\"scheduleId\":"); pw.print(scheduleId); pw.print(", "); pw.print("\"timeStamp\":"); pw.print(metric.getTimestamp()); pw.print(", "); pw.print("\"value\":"); pw.print(metric.getValue()); pw.print("}"); } pw.println("]"); } @Override public void write(OutputStream outputStream) throws IOException, WebApplicationException { MetricsDAO metricsDAO = sessionManager.getMetricsDAO(); Iterable<RawNumericMetric> resultSet = metricsDAO.findRawMetrics(scheduleId, startTime, endTime); PrintWriter pw = new PrintWriter(outputStream); if (mediaType.equals(MediaType.APPLICATION_JSON_TYPE)) { jsonOutput(pw, resultSet); } else if (mediaType.equals(MediaType.APPLICATION_XML_TYPE)) { pw.println("<collection>"); for (RawNumericMetric metric : resultSet) { pw.print(" <numericDataPoint scheduleId=\""); pw.print(scheduleId); pw.print("\" timeStamp=\""); pw.print(metric.getTimestamp()); pw.print("\" value=\""); pw.print(metric.getValue()); pw.println("\"/>"); } pw.println("</collection>"); } else if (mediaType.toString().equals("text/csv")) { pw.println("#schedule,timestamp,value"); for (RawNumericMetric metric : resultSet) { pw.print(scheduleId); pw.print(','); pw.print(metric.getTimestamp()); pw.print(','); pw.println(metric.getValue()); } } else if (mediaType.equals(MediaType.TEXT_HTML_TYPE)) { pw.println("<table>"); pw.print("<tr><th>time</th><th>value</th></tr>\n"); for (RawNumericMetric metric : resultSet) { pw.print(" <tr>"); pw.print("<td>"); pw.print(new Date(metric.getTimestamp())); pw.print("</td><td>"); pw.print(metric.getValue()); pw.print("</td>"); pw.println("</tr>"); } pw.println("</table>"); } else { jsonOutput(pw, resultSet); } pw.flush(); pw.close(); } } }