View Javadoc

1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *     http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing, software
13   * distributed under the License is distributed on an "AS IS" BASIS,
14   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15   * See the License for the specific language governing permissions and
16   * limitations under the License.
17   */
18  package org.apache.hadoop.chukwa.datacollection.agent.rest;
19  
20  import org.apache.hadoop.chukwa.datacollection.agent.ChukwaAgent;
21  import org.apache.hadoop.chukwa.datacollection.adaptor.AdaptorException;
22  import org.apache.hadoop.chukwa.datacollection.adaptor.Adaptor;
23  import org.apache.hadoop.chukwa.datacollection.OffsetStatsManager;
24  import org.apache.hadoop.chukwa.util.ExceptionUtil;
25  import org.apache.commons.lang.StringEscapeUtils;
26  import org.json.JSONObject;
27  import org.json.JSONException;
28  import org.apache.commons.logging.Log;
29  import org.apache.commons.logging.LogFactory;
30  
31  import javax.ws.rs.Path;
32  import javax.ws.rs.GET;
33  import javax.ws.rs.Produces;
34  import javax.ws.rs.PathParam;
35  import javax.ws.rs.QueryParam;
36  import javax.ws.rs.DELETE;
37  import javax.ws.rs.POST;
38  import javax.ws.rs.Consumes;
39  import javax.ws.rs.core.Context;
40  import javax.ws.rs.core.Response;
41  import javax.ws.rs.core.MediaType;
42  import javax.servlet.ServletContext;
43  import javax.servlet.http.HttpServletResponse;
44  import java.text.DecimalFormat;
45  import java.util.Map;
46  
47  /**
48   * JAX-RS controller to handle all HTTP request to the Agent that deal with adaptors.
49   *
50   * To return all adaptors:
51   *   GET /rest/v1/adaptor
52   *   Optional QS params: viewType=text|xml (default=xml)
53   *
54   * To return a single adaptor:
55   *   GET /rest/v1/adaptor/[adaptorId]
56   *   Optional QS params: viewType=text|xml (default=xml)
57   *
58   * To remove an adaptor:
59   *   DELETE /rest/v1/adaptor/[adaptorId]
60   *
61   * To add an adaptor:
62   *   POST /rest/v1/adaptor
63   *   Content-Type: application/json
64   *   Optional QS params: viewType=text|xml (default=xml)
65   *
66   *   { "DataType" : "foo",
67   *     "AdaptorClass" : "FooAdaptor",
68   *     "AdaptorParams" : "params",
69   *     "Offset"     : "0" }
70   *   The first 3 params above are the only required ones.
71   */
72  @Path("/adaptor")
73  public class AdaptorController {
74  
75    private static final DecimalFormat DECIMAL_FORMAT = new DecimalFormat();
76    private static final Log log = LogFactory.getLog(AdaptorController.class);
77  
78    static {
79      DECIMAL_FORMAT.setMinimumFractionDigits(2);
80      DECIMAL_FORMAT.setMaximumFractionDigits(2);
81      DECIMAL_FORMAT.setGroupingUsed(false);
82    }
83  
84    /**
85     * Adds an adaptor to the agent and returns the adaptor info.
86     * @param context servletContext
87     * @param viewType type of view to return (text|xml)
88     * @param postBody JSON post body
89     * @return Response object
90     */
91    @POST
92    @Consumes("application/json")
93    @Produces({"text/xml","text/plain"})
94    public Response addAdaptor(@Context ServletContext context,
95                               @QueryParam("viewType") String viewType,
96                               String postBody) {
97      ChukwaAgent agent = (ChukwaAgent)context.getAttribute("ChukwaAgent");
98  
99      if (postBody == null) return badRequestResponse("Empty POST body.");
100 
101     // parse the json.
102     StringBuilder addCommand = new StringBuilder("add ");
103     try {
104       JSONObject reqJson = new JSONObject(postBody);
105 
106       String dataType = reqJson.getString("DataType");
107       //TODO: figure out how to set this per-adaptor
108       //String cluster = reqJson.getString("Cluster");
109       String adaptorClass = reqJson.getString("AdaptorClass");
110 
111       String adaptorParams = fetchOptionalString(reqJson, "AdaptorParams");
112       long offset = fetchOptionalLong(reqJson, "Offset", 0);
113 
114       addCommand.append(adaptorClass).append(' ');
115       addCommand.append(dataType);
116       if (adaptorParams != null)
117         addCommand.append(' ').append(adaptorParams);
118       addCommand.append(' ').append(offset);
119 
120     } catch (JSONException e) {
121       return badRequestResponse("Invalid JSON passed: '" + postBody + "', error: " + e.getMessage());
122     }
123 
124     // add the adaptor
125     try {
126       String adaptorId = agent.processAddCommandE(addCommand.toString());
127 
128       return doGetAdaptor(agent, adaptorId, viewType);
129     } catch (AdaptorException e) {
130       return badRequestResponse("Could not add adaptor for postBody: '" + postBody +
131               "', error: " + e.getMessage());
132     }
133   }
134 
135   /**
136    * Remove an adaptor from the agent
137    * @param context ServletContext
138    * @param adaptorId id of adaptor to remove.
139    * @return Response object
140    */
141   @DELETE
142   @Path("/{adaptorId}")
143   @Produces({"text/plain"})
144   public Response removeAdaptor(@Context ServletContext context,
145                                 @PathParam("adaptorId") String adaptorId) {
146     ChukwaAgent agent = (ChukwaAgent)context.getAttribute("ChukwaAgent");
147 
148     // validate that we have an adaptorId
149     if (adaptorId == null) {
150       return badRequestResponse("Missing adaptorId.");
151     }
152 
153     // validate that we have a valid adaptorId
154     if (agent.getAdaptor(adaptorId) == null) {
155       return badRequestResponse("Invalid adaptorId: " + adaptorId);
156     }
157 
158     // stop the agent
159     agent.stopAdaptor(adaptorId, true);
160     return Response.ok().build();
161   }
162 
163   /**
164    * Get all adaptors
165    * @param context ServletContext
166    * @param viewType type of view to return (text|xml)
167    * @return Response object
168    */
169   @GET
170   @Produces({"text/xml", "text/plain"})
171   public Response getAdaptors(@Context ServletContext context,
172                               @QueryParam("viewType") String viewType) {
173     ChukwaAgent agent = (ChukwaAgent)context.getAttribute("ChukwaAgent");
174     return doGetAdaptor(agent, null, viewType);
175   }
176 
177   /**
178    * Get a single adaptor
179    * @param context ServletContext
180    * @param viewType type of view to return (text|xml)
181    * @param adaptorId id of the adaptor to return
182    * @return Response object
183    */
184   @GET
185   @Path("/{adaptorId}")
186   @Produces({"text/xml","text/plain"})
187   public Response getAdaptor(@Context ServletContext context,
188                              @QueryParam("viewType") String viewType,
189                              @PathParam("adaptorId") String adaptorId) {
190     ChukwaAgent agent = (ChukwaAgent)context.getAttribute("ChukwaAgent");
191     return doGetAdaptor(agent, adaptorId, viewType);
192   }
193 
194   /**
195    * Handles the common rendering logic of checking for view type and returning
196    * a Response with one or all adaptors.
197    * @return Response object
198    */
199   private Response doGetAdaptor(ChukwaAgent agent, String adaptorId, String viewType) {
200     if ("text".equals(viewType)) {
201       return textResponse(buildAdaptorText(agent, adaptorId));
202     }
203     else if ("xml".equals(viewType) || viewType == null) {
204       return xmlResponse(buildAdaptorXML(agent, adaptorId));
205     }
206     else {
207       return badRequestResponse("Invalid viewType: " + viewType);
208     }
209   }
210 
211   /**
212    * Renders info for one or all adaptors in XML.
213    */
214   protected String buildAdaptorXML(ChukwaAgent agent, String adaptorId) {
215 
216     StringBuilder out = new StringBuilder(
217             "<?xml version=\"1.0\" encoding=\"UTF-8\"?>");
218 
219     appendStartTag(out, "Response");
220 
221     if (adaptorId == null) {
222       Map<String, String> adaptorMap = agent.getAdaptorList();
223       appendStartTag(out, "Adaptors", "total", adaptorMap.size());
224 
225       for (String name : adaptorMap.keySet()) {
226         Adaptor adaptor = agent.getAdaptor(name);
227         appendAdaptorXML(out, agent, adaptor);
228       }
229 
230       appendEndTag(out, "Adaptors");
231     }
232     else {
233       Adaptor adaptor = agent.getAdaptor(adaptorId);
234       if (adaptor != null) {
235         appendAdaptorXML(out, agent, adaptor);
236       }
237       else {
238         appendElement(out, "Error", "Invalid adaptor id: " + adaptorId);
239       }
240     }
241 
242     appendEndTag(out, "Response");
243 
244     return out.toString();
245   }
246 
247   /**
248    * Renders info for one or all adaptors in plain text (YAML).
249    */
250   protected String buildAdaptorText(ChukwaAgent agent, String adaptorId) {
251     StringBuilder out = new StringBuilder();
252 
253     Map<String, String> adaptorMap = agent.getAdaptorList();
254     int indent = 0;
255 
256     if (adaptorId == null) {
257       appendNvp(out, indent, "adaptor_count", adaptorMap.size());
258       appendNvp(out, indent, "adaptors", "");
259 
260       indent += 4;
261       for(String name : adaptorMap.keySet()) {
262         Adaptor adaptor = agent.getAdaptor(name);
263         appendAdaptorText(out, indent, agent, adaptor);
264       }
265     }
266     else {
267       Adaptor adaptor = agent.getAdaptor(adaptorId);
268       if (adaptor != null) {
269         appendNvp(out, indent, "adaptor", "");
270         indent += 4;
271         appendAdaptorText(out, indent, agent, adaptor);
272       }
273       else {
274         appendNvp(out, indent, "error_message", "Invalid adaptor id: " + adaptorId, true);
275       }
276     }
277 
278     return out.toString();
279   }
280 
281   private void appendAdaptorText(StringBuilder out, int indent,
282                                  ChukwaAgent agent, Adaptor adaptor) {
283     appendNvp(out, indent, "- adaptor_id", agent.offset(adaptor).adaptorID());
284     appendNvp(out, indent, "data_type", adaptor.getType());
285     appendNvp(out, indent, "offset", agent.offset(adaptor).offset());
286     appendNvp(out, indent, "adaptor_class", adaptor.getClass().getName());
287     appendNvp(out, indent, "adaptor_params", adaptor.getCurrentStatus(), true);
288 
289     OffsetStatsManager adaptorStats = agent.getAdaptorStatsManager();
290 
291     appendNvp(out, indent, "average_rates", "");
292     indent += 4;
293     appendNvp(out, indent, "- rate",
294             DECIMAL_FORMAT.format(adaptorStats.calcAverageRate(adaptor,  60)));
295     appendNvp(out, indent, "interval", "60");
296     appendNvp(out, indent, "- rate",
297             DECIMAL_FORMAT.format(adaptorStats.calcAverageRate(adaptor,  300)));
298     appendNvp(out, indent, "interval", "300");
299     appendNvp(out, indent, "- rate",
300             DECIMAL_FORMAT.format(adaptorStats.calcAverageRate(adaptor,  600)));
301     appendNvp(out, indent, "interval", "600");
302     indent -= 4;
303   }
304 
305   private void appendAdaptorXML(StringBuilder out,
306                                ChukwaAgent agent, Adaptor adaptor) {
307     appendStartTag(out, "Adaptor",
308             "id", agent.offset(adaptor).adaptorID(),
309             "dataType", adaptor.getType(),
310             "offset", agent.offset(adaptor).offset());
311 
312     appendElement(out, "AdaptorClass", adaptor.getClass().getName());
313     appendElement(out, "AdaptorParams", adaptor.getCurrentStatus());
314 
315     OffsetStatsManager adaptorStats = agent.getAdaptorStatsManager();
316 
317     appendElement(out, "AverageRate",
318             DECIMAL_FORMAT.format(adaptorStats.calcAverageRate(adaptor,  60)),
319             "intervalSeconds", "60");
320     appendElement(out, "AverageRate",
321             DECIMAL_FORMAT.format(adaptorStats.calcAverageRate(adaptor,  300)),
322             "intervalSeconds", "300");
323     appendElement(out, "AverageRate",
324             DECIMAL_FORMAT.format(adaptorStats.calcAverageRate(adaptor,  600)),
325             "intervalSeconds", "600");
326 
327     appendEndTag(out, "Adaptor");
328   }
329 
330   // *** static helper methods below. could be moved into a util class ***
331 
332   //   * response handling *
333 
334   private static Response textResponse(Object content) {
335     return Response.ok(content, MediaType.TEXT_PLAIN).build();
336   }
337 
338   private static Response xmlResponse(String content) {
339     return Response.ok(content, MediaType.TEXT_XML).build();
340   }
341 
342   private static Response badRequestResponse(String content) {
343     return Response.status(HttpServletResponse.SC_BAD_REQUEST)
344                      .entity(content).build();
345   }
346 
347   //   * json handling *
348 
349   private static String fetchOptionalString(JSONObject json, String name) {
350     try {
351       return json.getString(name);
352     } catch (JSONException e) {
353       log.debug(ExceptionUtil.getStackTrace(e));
354     }
355     return null;
356   }
357 
358   private static long fetchOptionalLong(JSONObject json, String name, long defaultLong) {
359     try {
360       return json.getLong(name);
361     } catch (JSONException e) {
362       return defaultLong;
363     }
364   }
365 
366   //   * plain text response handling *
367 
368   /**
369    * Helper for appending name/value pairs to the ServletOutputStream in the
370    * format [name]: [value]
371    */
372   protected static void appendNvp(StringBuilder out,
373                                   String name, Object value) {
374     appendNvp(out, 0, name, value, false);
375   }
376 
377   /**
378    * Helper for appending name/value pairs to the ServletOutputStream in the
379    * format [name]: [value] with indent number of spaces prepended.
380    */
381   protected static void appendNvp(StringBuilder out, int indent,
382                                   String name, Object value) {
383     appendNvp(out, indent, name, value, false);
384   }
385 
386   /**
387    * Helper for appending name/value pairs to the ServletOutputStream in the
388    * format [name]: [value] with indent number of spaces prepended. Set
389    * stringLiteral=true if the value might contain special YAML characters.
390    */
391   protected static void appendNvp(StringBuilder out, int indent,
392                                   String name, Object value,
393                                   boolean stringLiteral) {
394 
395     if (name.startsWith("- ") && indent > 1) indent -= 2;
396 
397     indent(out, indent);
398     out.append(name);
399     out.append(": ");
400 
401     if (stringLiteral) {
402       out.append('|').append('\n');
403       indent(out, indent + 2);
404     }
405 
406     if (value != null)
407       out.append(value.toString()).append('\n');
408   }
409 
410   /**
411    * Helper to insert a number of spaces into the output stream.
412    */
413   protected static void indent(StringBuilder out, int indent) {
414     for (int i = 0; i < indent; i++) {
415       out.append(' ');
416     }
417   }
418 
419   //   * XML text response handling *
420 
421   /**
422    * XML helper to append a Element start tag. Optionally a number of attributeNvps
423    * can be passed, which will be inserted as XML atrribute names and values,
424    * alternating between names and values.
425    */
426   protected static void appendStartTag(StringBuilder out,
427                                        String name,
428                                        Object... attributeNvps) {
429     out.append("<");
430     out.append(name);
431     for(int i = 0; i < attributeNvps.length - 1; i = i + 2) {
432       out.append(" ");
433       out.append(attributeNvps[i].toString());
434       out.append("=\"");
435       if (attributeNvps[i + 1] != null)
436         out.append(StringEscapeUtils.escapeXml(attributeNvps[i + 1].toString()));
437       out.append("\"");
438     }
439     out.append(">");
440   }
441 
442   /**
443    * XML helper to append a Element end tag.
444    */
445   protected static void appendEndTag(StringBuilder out,
446                                      String name) {
447     out.append("</");
448     out.append(name);
449     out.append(">");
450   }
451 
452   /**
453    * XML helper to append an Element and it's child text value. Optionally a
454    * number of attributeNvps can be passed, which will be inserted as XML
455    * atrribute names and values, alternating between names and values.
456    */
457   protected static void appendElement(StringBuilder out,
458                                       String name, Object value,
459                                       Object... attributeNvps) {
460     appendStartTag(out, name, attributeNvps);
461     if (value != null)
462       out.append(StringEscapeUtils.escapeXml(value.toString()));
463     appendEndTag(out, name);
464   }
465 
466 }