/**
* Copyright (c) 2013 Puppet Labs, Inc. and other contributors, as listed below.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Apache License, Version 2.0
* which accompanies this distribution, and is available at
* http://www.apache.org/licenses/LICENSE-2.0
*
* Contributors:
* Puppet Labs
*/
package com.puppetlabs.puppetdb.javaclient.impl;
import java.io.IOException;
import java.lang.reflect.Type;
import java.net.URLEncoder;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import org.apache.commons.codec.binary.Hex;
import org.apache.http.HttpStatus;
import org.apache.http.client.HttpResponseException;
import com.google.gson.annotations.SerializedName;
import com.google.inject.Inject;
import com.puppetlabs.puppetdb.javaclient.HttpConnector;
import com.puppetlabs.puppetdb.javaclient.PuppetDBClient;
import com.puppetlabs.puppetdb.javaclient.model.*;
import com.puppetlabs.puppetdb.javaclient.model.EventCount.CountBy;
import com.puppetlabs.puppetdb.javaclient.model.EventCount.SummarizeBy;
import com.puppetlabs.puppetdb.javaclient.query.Expression;
import com.puppetlabs.puppetdb.javaclient.query.Paging;
import com.puppetlabs.puppetdb.javaclient.query.Parameters;
/**
* Default implementation of the PuppetDBClient
*/
public class PuppetDBClientImpl implements PuppetDBClient {
public static class ServerTime {
@SerializedName("server-time")
private Date serverTime;
}
public static class ServerVersion {
@SerializedName("version")
private String version;
}
/**
* Builds a path from the given key and all qualifiers. The key is expected to start, but not end
* with a slash. The qualifiers must not start nor end with a slash. The final string will not
* end with a slash.
*
* @param bld
* The receiver of the path
* @param key
* The key that will be first in the path
* @param qualifiers
* The optional qualifiers to append to the path
* @return The string representation of the path
*/
protected static String buildPath(StringBuilder bld, String key, String[] qualifiers) {
bld.append(key);
for(String qualifier : qualifiers) {
bld.append('/');
bld.append(qualifier);
}
return bld.toString();
}
private static Map<String, String> paramsAsMap(Parameters<?> params) {
if(params == null)
return null;
Map<String, String> queryParams = new HashMap<String, String>();
params.appendTo(queryParams);
return queryParams;
}
private final HttpConnector connector;
/**
* <p>
* Creates a new PuppetDBClient instance. This constructor
* </p>
* <p>
* <b>For Guice injection only.</b> Don't use this constructor from code
* </p>
*
* @param connector
* The connector responsible for all HTTP requests
*/
@Inject
public PuppetDBClientImpl(HttpConnector connector) {
this.connector = connector;
}
private void addEventCountParams(Parameters<EventCount> params, Expression<Event> eventQuery, SummarizeBy summarizeBy, CountBy countBy,
Map<String, String> queryMap) {
if(eventQuery != null)
eventQuery.appendTo(queryMap);
Map<String, String> ecMap = paramsAsMap(params);
if(ecMap != null) {
String filter = ecMap.remove("query");
if(filter != null)
queryMap.put("counts-filter", filter);
queryMap.putAll(ecMap);
}
if(summarizeBy == null)
summarizeBy = SummarizeBy.certname;
queryMap.put("summarize-by", summarizeBy.toString());
// count-by is optional and defaults to certname so we don't add it unless it differs
if(countBy != null && countBy != CountBy.certname)
queryMap.put("count-by", countBy.toString());
}
@Override
public UUID deactivateNode(String node) throws IOException {
return postCommand("deactivate node", 1, node);
}
@Override
public List<Node> getActiveNodes(Parameters<Node> params) throws IOException {
return getListResponse("/nodes", params, Node.LIST);
}
@Override
public AggregatedEventCount getAggregatedEventCounts(Expression<EventCount> eventCountQuery, Expression<Event> eventQuery,
SummarizeBy summarizeBy, CountBy countBy) throws IOException {
Map<String, String> queryMap = new HashMap<String, String>();
addEventCountParams(eventCountQuery, eventQuery, summarizeBy, countBy, queryMap);
return getSingletonResponse("/aggregate-event-counts", queryMap, AggregatedEventCount.class);
}
@Override
public List<EventCount> getEventCounts(final Parameters<EventCount> params, final Expression<Event> eventQuery,
final SummarizeBy summarizeBy, final CountBy countBy) throws IOException {
return getListResponse("/event-counts", new Parameters<EventCount>() {
@Override
public void appendTo(Map<String, String> queryParams) {
addEventCountParams(params, eventQuery, summarizeBy, countBy, queryParams);
}
}, EventCount.LIST);
}
@Override
public List<Event> getEvents(Parameters<Event> params) throws IOException {
return getListResponse("/events", params, Event.LIST);
}
@Override
public List<String> getFactNames() throws IOException {
return getListResponse("/fact-names", null, Entity.LIST_STRING);
}
@Override
public List<Fact> getFacts(Parameters<Fact> params, String... factQualifiers) throws IOException {
StringBuilder bld = new StringBuilder();
return getListResponse(buildPath(bld, "/facts", factQualifiers), params, Fact.LIST);
}
/**
* Executes the request and converts the result into a list of the desired <code>type</code>. If the request results in a
* {@link HttpStatus#SC_NOT_FOUND}, then this method will
* return an empty list.
*
* @param uriStr
* The relative path to the endpoint
* @param params
* Parameters to pass in the request
* @param type
* The expected return type (must be a generic List declaration)
* @param paging
* The paging info or <code>null</code>.
* @return The response in list form or an empty list in case no data was found
* @throws IOException
*/
protected <V, Q> List<V> getListResponse(String uriStr, Parameters<Q> params, Type type) throws IOException {
try {
List<V> result;
if(params instanceof Paging && ((Paging<?>) params).isIncludeTotal())
result = connector.get(uriStr, (Paging<Q>) params, type);
else
result = connector.get(uriStr, paramsAsMap(params), type);
return result;
}
catch(HttpResponseException e) {
if(e.getStatusCode() == HttpStatus.SC_NOT_FOUND)
return Collections.emptyList();
throw e;
}
}
/**
* Executes the request and converts the result into a map of the desired <code>type</code>. If the request results in a
* {@link HttpStatus#SC_NOT_FOUND}, then this method will
* return an empty map.
*
* @param uriStr
* The relative path to the endpoint
* @param params
* Parameters to pass in the request
* @param type
* The expected return type (must be a generic List declaration)
* @return The response in list form or an empty list in case no data was found
* @throws IOException
*/
protected <K, V> Map<K, V> getMapResponse(String uriStr, Map<String, String> params, Type type) throws IOException {
try {
return connector.get(uriStr, params, type);
}
catch(HttpResponseException e) {
if(e.getStatusCode() == HttpStatus.SC_NOT_FOUND)
return Collections.emptyMap();
throw e;
}
}
@Override
public Map<String, Object> getMetric(String metricName) throws IOException {
return getMapResponse("/metrics/mbean/" + URLEncoder.encode(metricName, HttpConnector.UTF_8.name()), null, Entity.MAP_STRING_OBJECT);
}
@Override
public Map<String, String> getMetrics() throws IOException {
return getMapResponse("/metrics/mbeans", null, Entity.MAP_STRING_STRING);
}
@Override
public List<Fact> getNodeFacts(Parameters<Fact> params, String node, String... factQualifiers) throws IOException {
StringBuilder bld = new StringBuilder("/nodes/");
bld.append(URLEncoder.encode(node, HttpConnector.UTF_8.name()));
return getListResponse(buildPath(bld, "/facts", factQualifiers), params, Fact.LIST);
}
@Override
public List<Resource> getNodeResources(Parameters<Resource> params, String node, String... resourceQualifiers) throws IOException {
StringBuilder bld = new StringBuilder("/nodes/");
bld.append(URLEncoder.encode(node, HttpConnector.UTF_8.name()));
return getListResponse(buildPath(bld, "/resources", resourceQualifiers), params, Resource.LIST);
}
@Override
public Node getNodeStatus(String node) throws IOException {
return getSingletonResponse("/nodes/" + URLEncoder.encode(node, HttpConnector.UTF_8.name()), null, Node.class);
}
@Override
public List<Report> getReports(Parameters<Report> params) throws IOException {
return getListResponse("/reports", params, Report.LIST);
}
@Override
public List<Resource> getResources(Parameters<Resource> params, String... resourceQualifiers) throws IOException {
StringBuilder bld = new StringBuilder();
return getListResponse(buildPath(bld, "/resources", resourceQualifiers), params, Resource.LIST);
}
@Override
public Date getServerTime() throws IOException {
ServerTime st = getSingletonResponse("/server-time", Collections.<String, String> emptyMap(), ServerTime.class);
return st.serverTime;
}
/**
* Executes the request and converts the result into an object of the desired <code>type</code>. If the request results in a
* {@link HttpStatus#SC_NOT_FOUND}, then this method will return <code>null</code>.
*
* @param uriStr
* The relative path to the endpoint
* @param params
* Parameters to pass in the request
* @param type
* The expected return type
* @return The response or <code>null</code> in case no data was found
* @throws IOException
*/
protected <V> V getSingletonResponse(String uriStr, Map<String, String> params, Type type) throws IOException {
try {
return connector.get(uriStr, params, type);
}
catch(HttpResponseException e) {
if(e.getStatusCode() == HttpStatus.SC_NOT_FOUND)
return null;
throw e;
}
}
@Override
public String getVersion() throws IOException {
ServerVersion sv = getSingletonResponse("/version", Collections.<String, String> emptyMap(), ServerVersion.class);
return sv.version;
}
protected UUID postCommand(String command, int version, Object payload) throws IOException {
CommandObject cmdObj = new CommandObject();
cmdObj.setCommand(command);
cmdObj.setVersion(version);
cmdObj.setPayload(payload);
String json = connector.toJSON(cmdObj);
Map<String, String> params = new HashMap<String, String>();
params.put("payload", json);
try {
MessageDigest md = MessageDigest.getInstance("SHA-1");
params.put("checksum", Hex.encodeHexString(md.digest(json.getBytes(HttpConnector.UTF_8))));
}
catch(NoSuchAlgorithmException e) {
throw new IllegalArgumentException(e);
}
CommandResponse response = connector.post("/commands/", params, CommandResponse.class);
return response == null
? null
: UUID.fromString(response.getUuid());
}
@Override
public UUID replaceCatalog(Catalog catalog) throws IOException {
return postCommand("replace catalog", 2, catalog);
}
@Override
public UUID replaceFacts(Facts facts) throws IOException {
// TODO: This is rather odd since we json encode something that will
// be json encoded again.
String jsonFacts = connector.toJSON(facts);
return postCommand("replace facts", 1, jsonFacts);
}
@Override
public UUID storeReport(Report report) throws IOException {
return postCommand("store report", 1, report);
}
}