/*
* Copyright 2009 Alberto Gimeno <gimenete at gmail.com>
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package siena.sdb.ws;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.net.URL;
import java.net.URLConnection;
import java.net.URLEncoder;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.TimeZone;
import java.util.TreeMap;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;
import org.xml.sax.Attributes;
import org.xml.sax.SAXException;
import org.xml.sax.helpers.DefaultHandler;
public class SimpleDB {
private static final String VERSION = "2009-04-15";
private static final String SIGNATURE_METHOD = "HmacSHA256";
private static final String SIGNATURE_VERSION = "2";
private static final String ENCODING = "UTF-8";
private static final String HOST = "sdb.amazonaws.com";
private static final String PROTOCOL = "http";
private static final String PATH = "/";
private static final String METHOD = "POST";
private String awsAccessKeyId = null;
private String awsSecretAccessKey = null;
private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'") {
private static final long serialVersionUID = 1L;
{
setTimeZone(TimeZone.getTimeZone("UTC"));
}
};
public SimpleDB(String awsAccessKeyId, String awsSecretAccessKey) {
this.awsAccessKeyId = awsAccessKeyId;
this.awsSecretAccessKey = awsSecretAccessKey;
}
private String sign(String data, String key) {
try {
SecretKeySpec signingKey = new SecretKeySpec(key.getBytes(ENCODING), SIGNATURE_METHOD);
Mac mac = Mac.getInstance(SIGNATURE_METHOD);
mac.init(signingKey);
byte[] rawHmac = mac.doFinal(data.getBytes(ENCODING));
return Base64.encodeBytes(rawHmac);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
private void signParams(String httpverb, String host, String uri, String secretAccessKey, TreeMap<String, String> parameters) {
parameters.put("AWSAccessKeyId", awsAccessKeyId);
parameters.put("SignatureVersion", SIGNATURE_VERSION);
parameters.put("SignatureMethod", SIGNATURE_METHOD);
parameters.put("Timestamp", timestamp());
parameters.put("Version", VERSION);
String signature = signature(httpverb, host, uri, parameters);
parameters.put("Signature", sign(signature, secretAccessKey));
}
private String query(TreeMap<String, String> parameters) {
if(parameters.isEmpty()) return "";
StringBuilder params = new StringBuilder();
for (String key : parameters.keySet()) {
params.append("&");
params.append(urlEncode(key));
params.append("=");
params.append(urlEncode(parameters.get(key)));
}
return params.toString().substring(1);
}
private String signature(String httpverb, String host, String uri, TreeMap<String, String> parameters) {
return httpverb+"\n"+host+"\n"+uri+"\n"+query(parameters);
}
private String urlEncode(String s) {
try {
return URLEncoder.encode(s, ENCODING).replace("+", "%20").replace("*", "%2A").replace("%7E", "~");
} catch (UnsupportedEncodingException e) {
throw new RuntimeException(e);
}
}
public Response createDomain(String domain) {
TreeMap<String, String> parameters = new TreeMap<String, String>();
parameters.put("Action", "CreateDomain");
parameters.put("DomainName", domain);
return request(parameters, new PlainHandler());
}
public Response putAttributes(String domain, Item item) {
TreeMap<String, String> parameters = new TreeMap<String, String>();
parameters.put("Action", "PutAttributes");
parameters.put("DomainName", domain);
parameters.put("ItemName", item.name);
int i = 0;
for (Map.Entry<String, List<String>> entry : item.attributes.entrySet()) {
List<String> values = entry.getValue();
for (String value : values) {
parameters.put("Attribute."+i+".Name", entry.getKey());
parameters.put("Attribute."+i+".Value", value);
parameters.put("Attribute."+i+".Replace", "true"); // FIXME always replaces
i++;
}
}
return request(parameters, new PlainHandler());
}
public GetAttributesResponse getAttributes(String domain, String itemName) {
TreeMap<String, String> parameters = new TreeMap<String, String>();
parameters.put("Action", "GetAttributes");
parameters.put("DomainName", domain);
parameters.put("ItemName", itemName);
return request(parameters, new GetAttributesHandler(itemName));
}
public ListDomainsResponse listDomains(String maxNumberOfDomains, String nextToken) {
TreeMap<String, String> parameters = new TreeMap<String, String>();
parameters.put("Action", "ListDomains");
if(maxNumberOfDomains != null)
parameters.put("MaxNumberOfDomains", maxNumberOfDomains);
if(nextToken != null)
parameters.put("NextToken", nextToken);
return request(parameters, new ListDomainsHandler());
}
public Response deleteDomain(String domain) {
TreeMap<String, String> parameters = new TreeMap<String, String>();
parameters.put("Action", "DeleteDomain");
parameters.put("DomainName", domain);
return request(parameters, new PlainHandler());
}
public DomainMetadataResponse domainMetadata(String domain) {
TreeMap<String, String> parameters = new TreeMap<String, String>();
parameters.put("Action", "DomainMetadata");
parameters.put("DomainName", domain);
return request(parameters, new DomainMetadataHandler());
}
public Response deleteAttributes(String domain, Item item) {
TreeMap<String, String> parameters = new TreeMap<String, String>();
parameters.put("Action", "DeleteAttributes");
parameters.put("DomainName", domain);
parameters.put("ItemName", item.name);
int i = 0;
for (Map.Entry<String, List<String>> entry : item.attributes.entrySet()) {
List<String> values = entry.getValue();
for (String value : values) {
parameters.put("Attribute."+i+".Name", entry.getKey());
parameters.put("Attribute."+i+".Value", value);
i++;
}
}
return request(parameters, new PlainHandler());
}
public SelectResponse select(String selectExpression, String nextToken) {
TreeMap<String, String> parameters = new TreeMap<String, String>();
parameters.put("Action", "Select");
if(selectExpression != null)
parameters.put("SelectExpression", selectExpression);
if(nextToken != null)
parameters.put("NextToken", nextToken);
return request(parameters, new SelectHandler());
}
private static String timestamp() {
return DATE_FORMAT.format(new Date());
}
private <T extends Response> T request(TreeMap<String, String> parameters, BasicHandler<T> handler) {
signParams(METHOD, HOST, PATH, awsSecretAccessKey, parameters);
try {
SAXParserFactory factory = SAXParserFactory.newInstance();
factory.setValidating(false);
factory.setNamespaceAware(false);
SAXParser parser = factory.newSAXParser();
URL url = new URL(PROTOCOL+"://"+HOST+PATH);
URLConnection connection = url.openConnection();
connection.setRequestProperty("Content-type", "application/x-www-form-urlencoded;charset="+ENCODING);
connection.setDoOutput(true);
connection.setDoInput(true);
OutputStream os = connection.getOutputStream();
// System.out.println(PROTOCOL+"://"+HOST+PATH+"?"+query(parameters));
os.write(query(parameters).getBytes(ENCODING));
os.close();
parser.parse(connection.getInputStream(), handler);
return handler.response;
} catch (Exception e) {
throw new RuntimeException(e);
}
}
public static String quote(String s) {
if(s != null){
return "\""+s.replace("'", "''")+"\"";
}
return null;
}
} abstract class BaseHandler extends DefaultHandler {
protected StringBuilder buffer = new StringBuilder();
@Override
public void characters(char[] ch, int start, int length) throws SAXException {
buffer.append(ch, start, length);
}
@Override
public void endElement(String uri, String localName, String name) throws SAXException {
read(name, buffer.toString());
buffer = new StringBuilder();
}
public abstract void read(String name, String value);
} class BasicHandler<T extends Response> extends BaseHandler {
protected T response = null;
public BasicHandler(T response) {
this.response = response;
}
@Override
public void read(String name, String value) {
if(name.equals("RequestId"))
response.metadata.requestId = value;
else if(name.equals("BoxUsage"))
response.metadata.boxUsage = value;
}
public T getResponse() {
return response;
}
} class PlainHandler extends BasicHandler<Response> {
public PlainHandler() {
super(new Response());
}
} class GetAttributesHandler extends BasicHandler<GetAttributesResponse> {
private String attrname;
private String attrvalue;
public GetAttributesHandler(String itemName) {
super(new GetAttributesResponse(itemName));
}
@Override
public void read(String name, String value) {
super.read(name, value);
if(name.equals("Attribute")) {
response.item.add(attrname, attrvalue);
} else if(name.equals("Name")) {
attrname = value;
} else if(name.equals("Value")) {
attrvalue = value;
}
}
} class ListDomainsHandler extends BasicHandler<ListDomainsResponse> {
public ListDomainsHandler() {
super(new ListDomainsResponse());
}
@Override
public void read(String name, String value) {
super.read(name, value);
if(name.equals("DomainName"))
response.domains.add(value);
}
} class DomainMetadataHandler extends BasicHandler<DomainMetadataResponse> {
public DomainMetadataHandler() {
super(new DomainMetadataResponse());
}
@Override
public void read(String name, String value) {
super.read(name, value);
if(name.equals("Timestamp"))
response.domainMetadata.timestamp = Long.parseLong(value);
else if(name.equals("ItemCount"))
response.domainMetadata.itemCount = Long.parseLong(value);
else if(name.equals("AttributeValueCount"))
response.domainMetadata.attributeValueCount = Long.parseLong(value);
else if(name.equals("AttributeNameCount"))
response.domainMetadata.attributeNameCount = Long.parseLong(value);
else if(name.equals("ItemNamesSizeBytes"))
response.domainMetadata.itemNamesSizeBytes = Long.parseLong(value);
else if(name.equals("AttributeValuesSizeBytes"))
response.domainMetadata.attributeValuesSizeBytes = Long.parseLong(value);
else if(name.equals("AttributeNamesSizeBytes"))
response.domainMetadata.attributeNamesSizeBytes = Long.parseLong(value);
}
} class SelectHandler extends BasicHandler<SelectResponse> {
private Item item;
private boolean attribute;
private String attrname;
private String attrvalue;
public SelectHandler() {
super(new SelectResponse());
}
@Override
public void startElement(String uri, String localName, String name,
Attributes attributes) throws SAXException {
super.startElement(uri, localName, name, attributes);
if(name.equals("Item")) {
item = new Item();
response.items.add(item);
} else if(name.equals("Attribute")) {
attribute = true;
}
}
@Override
public void read(String name, String value) {
super.read(name, value);
if(name.equals("Name")) {
if(attribute)
attrname = value;
else
item.name = value;
} else if(name.equals("Value")) {
attrvalue = value;
} else if(name.equals("NextToken")) {
response.nextToken = value;
} else if(name.equals("Attribute")) {
attribute = false;
item.add(attrname, attrvalue);
}
}
}