Jos Nieuwenhuis


Gebruik maken van Amazon’s Product Advertising API

In een zelfgemaakte applicatie maak ik gebruik van Amazon Product Advertising API om informatie over boeken op te halen. Jarenlang kon ik deze webservice zonder enige aanpassing gebruiken. Recentelijk is echter het authenticatie mechanisme gewijzigd. Amazon biedt twee mogelijkheden om de authenticatie toe te passen: met of zonder WS-Security. Als er geen gebruik wordt gemaakt van WS-Security moet de AWS identificatie in combinatie met een te berekenen hash worden meegestuurd in de SOAP Header.

In plaats van de bestaande code aan te passen besloot ik om deze weg te gooien en opnieuw te beginnen. Dit maal besloot ik om alles in een Maven artifact te stoppen. Dit biedt als voordeel dat elke IDE mits geïntegreerd met Maven deze library kan gaan gebruiken. Ik besloot om de library in NetBeans IDE 6.7 te programmeren. Dit bleek heel eenvoudig. Met behulp van de Web Service Client wizard kan het Maven POM bestand worden gegenereerd. In de wizard heb ik gekozen voor JAX-WS Style. De WSDL van de webservice staat op de volgende locatie: WSDL. Hieronder de gegenereerde POM:

<project>
  <modelVersion>4.0.0</modelVersion>
  <groupId>com.amazon.webservices</groupId>
  <artifactId>aws-ecommerce-service</artifactId>
  <packaging>jar</packaging>
  <version>2009_10_01</version>
  <name>AWSECommerceServicet</name>
  <build>
    <plugins>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-compiler-plugin</artifactId>
        <version>2.0.2</version>
        <configuration>
          <source>1.5</source>
          <target>1.5</target>
        </configuration>
      </plugin>
      <plugin>
        <groupId>org.codehaus.mojo</groupId>
        <artifactId>jaxws-maven-plugin</artifactId>
        <version>1.10</version>
        <executions>
          <execution>
            <goals>
              <goal>wsimport</goal>
            </goals>
            <configuration>
              <wsdlFiles>
                <wsdlFile>AWSECommerceService.wsdl</wsdlFile>
              </wsdlFiles>
              <staleFile>${project.build.directory}
              /jaxws/stale/AWSECommerceService.stale</staleFile>
            </configuration>
            <id>wsimport-generate-AWSECommerceService</id>
            <phase>generate-sources</phase>
          </execution>
        </executions>
        <dependencies>
          <dependency>
            <groupId>javax.xml</groupId>
            <artifactId>webservices-api</artifactId>
            <version>1.4</version>
          </dependency>
        </dependencies>
      </plugin>
    </plugins>
  </build>
  <dependencies>
    <dependency>
      <groupId>com.sun.xml.ws</groupId>
      <artifactId>webservices-rt</artifactId>
      <version>1.4</version>
      <scope>provided</scope>
    </dependency>
  </dependencies>
</project>

Met behulp van het Maven commando mvn clean install wordt de library gegenereerd, gecompileerd en geïnstalleerd in de lokale Maven repository.

In een ander Maven project gebruik ik deze library. Dit project fungeert als een soort adapter. Uiteindelijk wil ik vanuit de applicatie alleen een simpele interface aanspreken om boek-informatie op te halen:

public interface BookService {
  public abstract Book findByISBN(String isbn);
}

Het toevoegen van de authenticatie code bleek nog niet zo eenvoudig. Met behulp van een SOAPHandler is de Header van een SOAP bericht te manipuleren:

public Book findByISBN(String isbn) {
  AWSECommerceService awsecommerceservice = new AWSECommerceService();
  awsecommerceservice.setHandlerResolver(new HandlerResolver() {
    @Override
    public List<Handler> getHandlerChain(PortInfo portInfo) {
      List<Handler> handlerList = new ArrayList<Handler>();
      handlerList.add(new AmazonSOAPHandler());
      return handlerList;
    }
  });
  AWSECommerceServicePortType awsecommerceserviceport = 
      awsecommerceservice.getAWSECommerceServicePort();
  List<ItemLookupRequest> request = createItemLookupRequest(isbn);
  Holder<List<Items>> items = new Holder<List<Items>>();
  awsecommerceserviceport.itemLookup(null, AWS_ACCESS_KEY_ID, null, null, 
      null, null, null, request, null, items);
  return parseResults(items);
}

De SOAPHandler bevat de volgende methode om de Header aan te passen:

public boolean handleMessage(SOAPMessageContext context) {
  Boolean outboundProperty =
      (Boolean) context.get(MessageContext.MESSAGE_OUTBOUND_PROPERTY);
  if (outboundProperty.booleanValue()) {
    try {
      AmazonSOAPHeaderData headerData = new AmazonSOAPHeaderData();
      SOAPEnvelope envelope = context.getMessage().getSOAPPart().getEnvelope();
      SOAPHeader header = envelope.addHeader();
      headerData.addInformationToSOAPHeader(header);
    } catch (Exception ex) {
      Logger.getLogger(AmazonSOAPHandler.class.getName()).log(Level.SEVERE, null, ex);
    }
  } 
  return true;
}

AmazonSOAPHeaderData class bevat de informatie die in de header moet worden toegevoegd. Tevens wordt de ‘handtekening’ (Signature) in deze class berekend. Deze is bijna identiek aan het algoritme in de documenentatie van Amazon. Ik moest een kleine aanpassing doen om de code correct te laten werken:

public class AmazonSOAPHeaderData {

    private static final String UTF8_CHARSET = "UTF-8";
    private static final String HMAC_SHA256_ALGORITHM = "HmacSHA256";
    private String awsAccessKeyId = "XXXXXXXXXXXXXXXXXXXX";
    private String awsSecretKey = "SSSSSSSSSSSSSSSSSSSSSSSS";
    private String action = "ItemLookup";
    private static final String prefix = "aws";
    private static final String uri = "http://security.amazonaws.com/doc/2007-01-01/";
    private SecretKeySpec secretKeySpec = null;
    private Mac mac = null;

    public AmazonSOAPHeaderData() {
        try {
            byte[] secretyKeyBytes = awsSecretKey.getBytes(UTF8_CHARSET);
            secretKeySpec = new SecretKeySpec(secretyKeyBytes, HMAC_SHA256_ALGORITHM);
            mac = Mac.getInstance(HMAC_SHA256_ALGORITHM);
            mac.init(secretKeySpec);
        } catch (Exception e) {
            throw new RuntimeException(HMAC_SHA256_ALGORITHM + " is unsupported!", e);
        } 
    }

    private String hmac(String stringToSign) {
        String sig = "";
        byte[] data;
        byte[] rawHmac;
        try {
            data = stringToSign.getBytes(UTF8_CHARSET);
            rawHmac = mac.doFinal(data);
            Base64 encoder = new Base64();
            sig = new String(encoder.encode(rawHmac));
        } catch (UnsupportedEncodingException e) {
            throw new RuntimeException(UTF8_CHARSET + " is unsupported!", e);
        }
        return sig.trim();
    }

    private String createTimestamp() {
        Calendar cal = Calendar.getInstance();
        DateFormat dfm = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'");
        dfm.setTimeZone(TimeZone.getTimeZone("GMT"));
        return dfm.format(cal.getTime());
    }

    public void addInformationToSOAPHeader(SOAPHeader header) throws SOAPException {
        SOAPFactory factory = SOAPFactory.newInstance();
        String timestamp = createTimestamp();
        String signature = hmac(action + timestamp);

        SOAPElement accessKeyElem =
                factory.createElement("AWSAccessKeyId", prefix, uri);
        accessKeyElem.addTextNode(awsAccessKeyId);
        SOAPElement timestampElem =
                factory.createElement("Timestamp", prefix, uri);
        timestampElem.addTextNode(timestamp);
        SOAPElement signatureElem =
                factory.createElement("Signature", prefix, uri);
        signatureElem.addTextNode(signature);

        header.addChildElement(accessKeyElem);
        header.addChildElement(timestampElem);
        header.addChildElement(signatureElem);
    }
}

Met deze code is het mogelijk om boekinformatie op te halen. De complete broncode staat hier. Uiteraard moet je om gebruik te maken van de Product Advertising API een account aanvragen. Dit is gratis en staat verder beschreven op de website van de Amazon WebServices.

Datum: 18 oktober 2009 - Java,SOA

Geen reacties

No comments yet.

RSS feed for comments on this post.

Sorry, the comment form is closed at this time.