001    package org.hackystat.telemetry.service.resource.chart;
002    
003    import java.io.StringWriter;
004    import java.math.BigInteger;
005    
006    import javax.xml.bind.JAXBContext;
007    import javax.xml.bind.Marshaller;
008    import javax.xml.datatype.XMLGregorianCalendar;
009    import javax.xml.parsers.DocumentBuilder;
010    import javax.xml.parsers.DocumentBuilderFactory;
011    import javax.xml.transform.Transformer;
012    import javax.xml.transform.TransformerFactory;
013    import javax.xml.transform.dom.DOMSource;
014    import javax.xml.transform.stream.StreamResult;
015    
016    import org.hackystat.dailyprojectdata.client.DailyProjectDataClient;
017    import org.hackystat.sensorbase.client.SensorBaseClient;
018    import org.hackystat.sensorbase.resource.projects.ProjectUtils;
019    import org.hackystat.sensorbase.resource.projects.jaxb.Project;
020    import org.hackystat.sensorbase.resource.users.jaxb.User;
021    import org.hackystat.telemetry.analyzer.configuration.TelemetryChartDefinitionInfo;
022    import org.hackystat.telemetry.analyzer.configuration.TelemetryDefinitionManager;
023    import org.hackystat.telemetry.analyzer.configuration.TelemetryDefinitionManagerFactory;
024    import org.hackystat.telemetry.analyzer.configuration.TelemetryDefinitionType;
025    import org.hackystat.telemetry.analyzer.evaluator.TelemetryChartObject;
026    import org.hackystat.telemetry.analyzer.evaluator.TelemetryDefinitionResolver;
027    import org.hackystat.telemetry.analyzer.evaluator.TelemetryEvaluator;
028    import org.hackystat.telemetry.analyzer.evaluator.TelemetryStreamsObject;
029    import org.hackystat.telemetry.analyzer.evaluator.VariableResolver;
030    import org.hackystat.telemetry.analyzer.evaluator.TelemetryChartObject.SubChart;
031    import org.hackystat.telemetry.analyzer.evaluator.TelemetryStreamsObject.Stream;
032    import org.hackystat.telemetry.analyzer.language.ast.StringConstant;
033    import org.hackystat.telemetry.analyzer.language.ast.TelemetryChartDefinition;
034    import org.hackystat.telemetry.analyzer.language.ast.Variable;
035    import org.hackystat.telemetry.analyzer.model.TelemetryDataPoint;
036    import org.hackystat.telemetry.service.resource.chart.jaxb.Parameter;
037    import org.hackystat.telemetry.service.resource.chart.jaxb.TelemetryChartData;
038    import org.hackystat.telemetry.service.resource.chart.jaxb.TelemetryPoint;
039    import org.hackystat.telemetry.service.resource.chart.jaxb.TelemetryStream;
040    import org.hackystat.telemetry.service.resource.chart.jaxb.YAxis;
041    import org.hackystat.telemetry.service.resource.telemetry.TelemetryResource;
042    import org.hackystat.utilities.time.interval.DayInterval;
043    import org.hackystat.utilities.time.interval.Interval;
044    import org.hackystat.utilities.time.interval.MonthInterval;
045    import org.hackystat.utilities.time.interval.WeekInterval;
046    import org.hackystat.utilities.tstamp.Tstamp;
047    import org.restlet.Context;
048    import org.restlet.data.MediaType;
049    import org.restlet.data.Request;
050    import org.restlet.data.Response;
051    import org.restlet.resource.Representation;
052    import org.restlet.resource.Variant;
053    import org.w3c.dom.Document;
054    
055    /**
056     * Processes GET {host}/chart/{chart}/{email}/{project}/{granularity}/{start}/{end} and
057     * GET {host}/chart/{chart}/{email}/{project}/{granularity}/{start}/{end}?params={params} requests.
058     * Requires the authenticated user to be {email} or else the Admin user for the sensorbase connected
059     * to this service.
060     * 
061     * @author Philip Johnson
062     */
063    public class ChartDataResource extends TelemetryResource {
064      
065      private XMLGregorianCalendar startDay; 
066      private XMLGregorianCalendar endDay;
067    
068      /**
069       * The standard constructor.
070       * 
071       * @param context The context.
072       * @param request The request object.
073       * @param response The response object.
074       */
075      public ChartDataResource(Context context, Request request, Response response) {
076        super(context, request, response);
077      }
078    
079      /**
080       * Returns an TelemetryChart instance representing the trend information associated with the
081       * Project data, or null if not authorized.
082       * 
083       * @param variant The representational variant requested.
084       * @return The representation.
085       */
086      @Override
087      public Representation represent(Variant variant) {
088        if (variant.getMediaType().equals(MediaType.TEXT_XML)) {
089          try {
090            
091            // [1] Authenticate to DPD and SensorBase services; return with error if failure.
092            DailyProjectDataClient dpdClient = this.getDailyProjectDataClient();
093            SensorBaseClient sensorBaseClient = this.getSensorBaseClient();
094            try {
095              dpdClient.authenticate();
096              sensorBaseClient.authenticate();
097            }
098            catch (Exception e) {
099              setStatusError("Cannot authenticate this user", e);
100              return null;
101            }
102            
103            // [2] Get the User representation, return with error if not defined. 
104            User user;
105            try {
106              user = sensorBaseClient.getUser(this.uriUser);
107            }
108            catch (Exception e) {
109              setStatusError("Undefined user: " + this.uriUser, e);
110              return null;
111            }
112            
113            // [3] get the Project representation; return with error if not defined.
114            Project project;
115            try {
116              project = sensorBaseClient.getProject(this.uriUser, this.projectName);
117            }
118            catch (Exception e) {
119              setStatusError(String.format("Undefined project %s for owner %s", 
120                  this.projectName, uriUser), e);
121              return null;
122            }
123            
124            // [4] Get the chart representation, return with error if chart not defined.
125            TelemetryChartDefinition chartDef;
126            try {
127              TelemetryDefinitionManager manager = 
128                TelemetryDefinitionManagerFactory.getGlobalPersistentInstance();
129              TelemetryChartDefinitionInfo chartDefInfo = (TelemetryChartDefinitionInfo) manager.get(
130                  user, this.chart, true, TelemetryDefinitionType.CHART);
131              chartDef = chartDefInfo.getChartDefinitionObject();
132            }
133            catch (Exception e) {
134              setStatusError("Undefined chart " + this.chart, e);
135              return null;
136            }
137            
138            // [5] Get the start and end days, return with error if cannot be parsed.
139            try {
140              this.startDay = Tstamp.makeTimestamp(this.start);
141            }
142            catch (Exception e) {
143              setStatusError("Bad start day: " + this.start, e);
144              return null;
145            }
146            try {
147              this.endDay = Tstamp.makeTimestamp(this.end);
148            }
149            catch (Exception e) {
150              setStatusError("Bad end day: " + this.end, e);
151              return null;
152            }
153            
154            // [5.5] Make sure start and end days are OK w.r.t. project times.
155            if (!ProjectUtils.isValidStartTime(project, this.startDay)) {
156              String msg = this.startDay + " is before Project start day: " + project.getStartTime();
157              setStatusError(msg);
158              return null;
159            }
160            if (!ProjectUtils.isValidEndTime(project, this.endDay)) {
161              String msg = this.endDay + " is after Project end day: " + project.getEndTime();
162              setStatusError(msg);
163              return null;
164            }
165            if (Tstamp.lessThan(this.endDay, this.startDay)) {
166              String msg = this.startDay + " must be greater than: " + this.endDay;
167              setStatusError(msg);
168              return null;
169            }
170            
171            // [5.6] Telemetry end date cannot be after tomorrow. 
172            XMLGregorianCalendar tomorrow = Tstamp.incrementDays(Tstamp.makeTimestamp(), 1);
173            if (Tstamp.greaterThan(this.endDay, tomorrow)) {
174              String msg = this.endDay + " cannot be in the future. Change to today at the latest.";
175              setStatusError(msg);
176              return null;
177            }
178            
179            // [6] Create the appropriate interval based upon granularity, or return error.
180            Interval interval;
181            try {
182              if ("day".equalsIgnoreCase(this.granularity)) {
183                interval = new DayInterval(startDay, endDay);
184              }
185              else if ("week".equalsIgnoreCase(this.granularity)) {
186                interval = new WeekInterval(startDay, endDay);
187              }
188              else if ("month".equalsIgnoreCase(this.granularity)) {
189                interval = new MonthInterval(startDay, endDay);
190              }
191              else {
192                String msg = this.granularity + " must be either 'day', 'week', or 'month'";
193                setStatusError(msg);
194                return null;
195              }
196            }
197            catch (Exception e) {
198              String msg = this.start + " and " + this.end + " are illegal. Maybe out of order?";
199              setStatusError(msg, e);
200              return null;
201            }
202            
203            // [7] Get the parameters.
204            StringConstant[] varValues = parseParams(this.params);
205    
206            // [8] Check that supplied parameters match required parameters.
207            Variable[] variables = chartDef.getVariables();
208            if (varValues.length != variables.length) {
209              String msg = "Chart needs " + variables.length + " variables; got: " + varValues.length;
210              setStatusError(msg);
211              return null;
212            }
213            // Bind variables to values. 
214            VariableResolver variableResolver = new VariableResolver();
215            for (int i = 0; i < variables.length; i++) {
216              variableResolver.add(variables[i], varValues[i]);
217            }
218    
219            // [9] Make a telemetry definition resolver and generate the TelemetryChartObject.
220            TelemetryDefinitionResolver telemetryDefinitionResolver = new TelemetryDefinitionResolver(
221                TelemetryDefinitionManagerFactory.getGlobalPersistentInstance(), user);
222            TelemetryChartObject chartObject = TelemetryEvaluator.evaluate(chartDef,
223                telemetryDefinitionResolver, variableResolver, project, dpdClient, interval);
224            
225            // [10] Convert the TelemetryChartObject into it's "resource" representation.
226            TelemetryChartData chart = convertChartObjectToResource(chartObject);
227            
228            // [11] Add information about the variables and parameters to the resource.
229            for (int i = 0; i < variables.length; i++) {
230              Parameter parameter = new Parameter();
231              parameter.setName(variables[i].getName());
232              parameter.setValue(varValues[i].getValue());
233              chart.getParameter().add(parameter);
234            }
235    
236            // [12] package the resulting data up in XML and return it.
237            logRequest();
238            return this.getStringRepresentation(makeChartXml(chart));
239          }
240          catch (Exception e) {
241            setStatusError("Error processing chart", e);
242            return null;
243          }
244        }
245        // Shouldn't ever get here. 
246        return null;
247      }
248    
249      /**
250       * Takes the TelemetryChartObject returned from the telemetry analyzer code and converts it
251       * into the resource representation. 
252       * @param chartObject The TelemetryChartObject
253       * @return A TelemetryChart resource representation. 
254       */
255      private TelemetryChartData convertChartObjectToResource(TelemetryChartObject chartObject) {
256        // All of the JAXB instances will have variable names ending with "Resource" in order to
257        // distinguish them from their Telemetry Analyzer counterparts.  
258        TelemetryChartData chartResource = new TelemetryChartData();
259        chartResource.setURI(this.telemetryServer.getHostName() + "chart/" + this.chart +
260            "/" + this.uriUser + "/" + this.projectName + "/" + this.granularity + "/" +
261            this.start + "/" + this.end + "/");
262        //Not dealing with <Parameter> yet.
263        // for each Analyzer subchart...
264        for (SubChart subChart : chartObject.getSubCharts()) {
265          // Get the Analyzer YAxis instance. 
266          org.hackystat.telemetry.analyzer.evaluator.TelemetryChartObject.YAxis yaxis = 
267            subChart.getYAxis();
268          YAxis yAxisResource = new YAxis();
269          yAxisResource.setName("Unknown");
270          yAxisResource.setUnits(yaxis.getLabel());
271          if (yaxis.getLowerBound() != null) {
272            yAxisResource.setLowerBound(BigInteger.valueOf(yaxis.getLowerBound().longValue()));
273          }
274          if (yaxis.getUpperBound() != null) {
275            yAxisResource.setUpperBound(BigInteger.valueOf(yaxis.getUpperBound().longValue()));
276          }
277          
278          yAxisResource.setNumberType((yaxis.isIntegerAxis() ? "integer" : "double"));
279    
280          // Get the StreamsObject inside the SubChart.
281          TelemetryStreamsObject streams = subChart.getTelemetryStreamsObject();
282          // Get each Stream inside the StreamsObject...
283          for (Stream streamObject : streams.getStreams()) {
284            // Create a TelemetryStream Resource to associate with the Analyzer streamObject
285            TelemetryStream telemetryStreamResource = new TelemetryStream();
286            telemetryStreamResource.setYAxis(yAxisResource);
287            chartResource.getTelemetryStream().add(telemetryStreamResource);
288            telemetryStreamResource.setName(streamObject.getName());
289            // Now get the Analyzer TelemetryStream 'model' instance associated with the streamObject.
290            org.hackystat.telemetry.analyzer.model.TelemetryStream stream 
291            = streamObject.getTelemetryStream();
292            // Now iterate through the Analyzer TelemetryDataPoint instances. 
293            for (TelemetryDataPoint dataPoint : stream.getDataPoints()) {
294              // Create a resource DataPoint.
295              TelemetryPoint pointResource = new TelemetryPoint();
296              pointResource.setTime(Tstamp.makeTimestamp(dataPoint.getPeriod().getFirstDay()));
297              String val = (dataPoint.getValue() == null) ? null : dataPoint.getValue().toString();
298              pointResource.setValue(val);
299              telemetryStreamResource.getTelemetryPoint().add(pointResource);
300            }
301          }
302        }      
303        return chartResource;
304      }
305    
306      /**
307       * Creates a fake TelemetryChart instance with reasonable looking internal data.
308       * 
309       * @return A TelemetryChart instance.
310       * @throws Exception If problems occur in Tstamp.
311       */
312      @SuppressWarnings("unused")
313      private TelemetryChartData makeSampleChart() throws Exception {
314        TelemetryChartData chart = new TelemetryChartData();
315        // Set the attributes
316        chart.setURI(this.telemetryServer.getHostName() + "chart/" + this.chart);
317        // Set the parameter
318        Parameter parameter = new Parameter();
319        parameter.setName("FilePattern");
320        parameter.setValue("**");
321        chart.getParameter().add(parameter);
322        // Create telemetry points.
323        TelemetryPoint point1 = new TelemetryPoint();
324        point1.setTime(Tstamp.makeTimestamp("2007-08-01"));
325        TelemetryPoint point2 = new TelemetryPoint();
326        point2.setTime(Tstamp.makeTimestamp("2007-08-02"));
327        TelemetryPoint point3 = new TelemetryPoint();
328        point1.setTime(Tstamp.makeTimestamp("2007-08-03"));
329        // Create telemetry stream
330        TelemetryStream stream = new TelemetryStream();
331        stream.getTelemetryPoint().add(point1);
332        stream.getTelemetryPoint().add(point2);
333        stream.getTelemetryPoint().add(point3);
334        // Set the stream.
335        chart.getTelemetryStream().add(stream);
336        return chart;
337      }
338    
339      /**
340       * Returns the passed TelemetryChart instance as a String encoding of its XML representation.
341       * 
342       * @param data The SensorData instance.
343       * @return The XML String representation.
344       * @throws Exception If problems occur during translation.
345       */
346      private String makeChartXml(TelemetryChartData data) throws Exception {
347        JAXBContext chartJAXB = (JAXBContext) this.telemetryServer.getContext().getAttributes().get(
348            "ChartJAXB");
349        Marshaller marshaller = chartJAXB.createMarshaller();
350        DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
351        dbf.setNamespaceAware(true);
352        DocumentBuilder documentBuilder = dbf.newDocumentBuilder();
353        Document doc = documentBuilder.newDocument();
354        marshaller.marshal(data, doc);
355        DOMSource domSource = new DOMSource(doc);
356        StringWriter writer = new StringWriter();
357        StreamResult result = new StreamResult(writer);
358        TransformerFactory tf = TransformerFactory.newInstance();
359        Transformer transformer = tf.newTransformer();
360        transformer.transform(domSource, result);
361        return writer.toString();
362      }
363      
364      /**
365       * Parses the params parameter and returns the comma-separated values as an array of 
366       * StringConstant. 
367       * White spaces at the beginning and end of those substrings will be trimmed. 
368       * If a substring has single quotes or double quotes surrounding it, they are removed 
369       * before returning.
370       * <p>
371       * If you really want to preserve white spaces at the beginning or end of the substring, 
372       * use ' sub ' or " sub ".
373       * 
374       * @param input A comma-separated strings.
375       * @return An array of StringConstant.
376       */
377      private StringConstant[] parseParams(String input) {
378        String singleQuote = "'";
379        String doubleQuote = "\"";
380        if (input == null || input.length() == 0) {
381          return new StringConstant[0];
382        }
383        else {
384          String[] subs = input.split(",");
385          StringConstant[] constants = new StringConstant[subs.length];
386          for (int i = 0; i < subs.length; i++) {
387            String str = subs[i].trim();
388            if ((str.startsWith(singleQuote) && str.endsWith(singleQuote))
389                || (str.startsWith(doubleQuote) && str.endsWith(doubleQuote))) {
390              constants[i] = new StringConstant(str.substring(1, str.length() - 1));
391            }
392            else {
393              constants[i] = new StringConstant(str);
394            }
395          }
396          return constants;
397        }
398      }
399    
400    }