// BlogBridge -- RSS feed reader, manager, and web based service // Copyright (C) 2002-2006 by R. Pito Salas // // This program is free software; you can redistribute it and/or modify it under // the terms of the GNU General Public License as published by the Free Software Foundation; // either version 2 of the License, or (at your option) any later version. // // This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; // without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. // See the GNU General Public License for more details. // // You should have received a copy of the GNU General Public License along with this program; // if not, write to the Free Software Foundation, Inc., 59 Temple Place, // Suite 330, Boston, MA 02111-1307 USA // // Contact: R. Pito Salas // mailto:pitosalas@users.sourceforge.net // More information: about BlogBridge // http://www.blogbridge.com // http://sourceforge.net/projects/blogbridge // // $Id: AmazonGateway.java,v 1.6 2007/02/06 15:33:00 spyromus Exp $ // package com.salas.bb.utils.amazon; import com.salas.bb.utils.i18n.Strings; import org.jdom.Document; import org.jdom.Element; import org.jdom.JDOMException; import org.jdom.input.SAXBuilder; import java.io.IOException; import java.net.MalformedURLException; import java.net.URL; import java.text.MessageFormat; import java.util.ArrayList; import java.util.List; import java.util.logging.Level; import java.util.logging.Logger; /** * Gateway to Amazon.com. */ public class AmazonGateway { private static final Logger LOG = Logger.getLogger(AmazonGateway.class.getName()); private static final int ITEMS_PER_PAGE = 10; // The FAQ on AWS says that 1 second between calls is a guideline only that's why // we lower this number because there will be no stations generating continuous load private static final int TIME_BETWEEN_CALLS_MS = 10; private static final MessageFormat ITEM_SEARCH_FORMAT; private static final MessageFormat ITEM_URL_FORMAT; private String subscriptionId; private String affilateId; static { ITEM_SEARCH_FORMAT = new MessageFormat("http://webservices.amazon.com/onca/xml" + "?Service=AWSECommerceService" + "&SubscriptionId={0}" + "&Operation=ItemSearch" + "&Keywords={1}" + "&SearchIndex={2}" + "&Sort={3}" + "&ItemPage={4}" + "&ResponseGroup=Small,OfferSummary,Images,ItemAttributes"); ITEM_URL_FORMAT = new MessageFormat("http://www.amazon.com/exec/obidos/ASIN/{0}/{1}/"); } /** * Creates gateway to talk to <code>amazon.com</code>. * * @param subscriptionId subscription ID to issue AWS queries. * @param affilateId affilate ID to produce referrence links. */ public AmazonGateway(String subscriptionId, String affilateId) { this.subscriptionId = subscriptionId; this.affilateId = affilateId; } /** * <p>Returns collection of items from <code>Amazon.com</code>. <code>searchIndex</code> * defines the category of search, <code>keywords</code> define list of associated keywords, * <code>sort</code> is category-specific sort order.</p> * * <p>This method is synchronized as it's vital to have 1 second between calls -- terms of * contract. If multiple threads will be accessing this method we will break the contract.</p> * * @param keywords list of keywords separated with spaces. * @param searchIndex search index. * @param sort sort order or <code>NULL</code> for original sorting. * @param maxItems maximum number of items to return. * * @return list of {@link AmazonItem} instances. * * @throws AmazonException if fetching cannot be finished for some reason. */ public synchronized List<AmazonItem> itemsSearch(String keywords, AmazonSearchIndex searchIndex, String sort, int maxItems) throws AmazonException { ArrayList<AmazonItem> items = new ArrayList<AmazonItem>(maxItems); int pages = (int)Math.ceil(maxItems / (double)ITEMS_PER_PAGE); // We fetch items in pages. Pause between queries should be at least 1 second. // We continue fetching to the next page if only the previous page had full list of 10 // items. for (int page = 0; (items.size() == page * ITEMS_PER_PAGE) && page < pages; page++) { if (page > 0) sleepBetweenCalls(); items.addAll(fetchItemsPage(keywords, searchIndex, sort, page + 1)); } return items; } /** * Fetches the items corresponding to keywords, index type and sort order from the given page. * * @param keywords list of keywords separated with spaces. * @param searchIndex search index. * @param sort sort order or <code>NULL</code> for original sorting. * @param page page number (first is 1). * * @return items from the page. * * @throws AmazonException if fetching cannot be finished for some reason. */ private List<AmazonItem> fetchItemsPage(String keywords, AmazonSearchIndex searchIndex, String sort, int page) throws AmazonException { List<AmazonItem> itemsOnThePage = new ArrayList<AmazonItem>(ITEMS_PER_PAGE); String restCallURL = ITEM_SEARCH_FORMAT.format(new Object[] { subscriptionId, keywords.replaceAll(" ", "+"), searchIndex.toString(), sort, Integer.toString(page) }); Document doc = getResponseDocument(restCallURL); Element rootEl = doc.getRootElement(); clearNamespace(rootEl); Element itemsEl = rootEl.getChild("Items"); if (itemsEl != null) { List itemsElements = itemsEl.getChildren("Item"); for (Object itemsElement : itemsElements) { Element itemElement = (Element)itemsElement; try { itemsOnThePage.add(convertElementToItem(itemElement, searchIndex)); } catch (MalformedURLException e) { LOG.log(Level.WARNING, Strings.error("amazon.failed.to.create.item"), e); } } } return itemsOnThePage; } /** * This method ensures that we have valid document or error. It handles server errors from * 5xx series and retries automatically when it gets one. It respects the agreement to * call service no faster than 1 time a second. * * @param requestURL request URL. * * @return response document. * * @throws AmazonException in case when commnication failed or invalid document returned. */ private Document getResponseDocument(String requestURL) throws AmazonException { SAXBuilder builder = new SAXBuilder(false); Document doc = null; boolean retry = true; while (retry) { try { doc = builder.build(new URL(requestURL)); retry = false; } catch (IOException e) { String message = e.getMessage(); if (message != null && message.indexOf("code: 5") != -1) { LOG.warning(MessageFormat.format(Strings.error("amazon.ioexception.calling.amazon.service.retrying"), message)); sleepBetweenCalls(); retry = true; } else throw new AmazonException(e); } catch (JDOMException e) { throw new AmazonException(e); } } return doc; } /** * Clears namespace from this element and all of its sub-elements. * * @param element element. */ private void clearNamespace(Element element) { if (element == null) return; element.setNamespace(null); for (Object o : element.getChildren()) clearNamespace((Element)o); } /** * Converts response element to the item. * * @param itemElement item element. * @param searchIndex search index type. * * @return amazon item object. * * @throws MalformedURLException if item link is not correct. */ private AmazonItem convertElementToItem(Element itemElement, AmazonSearchIndex searchIndex) throws MalformedURLException { String asin = itemElement.getChildText("ASIN"); URL url = new URL(ITEM_URL_FORMAT.format(new Object[] { asin, affilateId })); AmazonItem item = new AmazonItem(asin, url, searchIndex); addAttributesToItem(item, itemElement.getChild("ItemAttributes")); addOfferSummaryToItem(item, itemElement); addImagesToItem(item, itemElement); return item; } /** * Takes attributes from ItemAttributes element and adds them to item. * * @param item item. * @param attributesElement item attributes element. */ private void addAttributesToItem(AmazonItem item, Element attributesElement) { if (attributesElement != null) { List children = attributesElement.getChildren(); for (Object aChildren : children) { Element attributeElement = (Element)aChildren; item.addAttribute(attributeElement.getName(), attributeElement.getTextTrim()); } } } /** * Adds offer summary information to the item. * * @param item item. * @param itemElement offer summary. */ private void addOfferSummaryToItem(AmazonItem item, Element itemElement) { Element itemAttributesElement = itemElement.getChild("ItemAttributes"); if (itemAttributesElement != null) { item.setListPrice(getPrice(itemAttributesElement.getChild("ListPrice"))); } Element offerSummaryElement = itemElement.getChild("OfferSummary"); if (offerSummaryElement != null) { item.setLowestNewPrice(getPrice(offerSummaryElement.getChild("LowestNewPrice"))); item.setLowestUsedPrice(getPrice(offerSummaryElement.getChild("LowestUsedPrice"))); } } /** * Takes the price from element. * * @param priceElement price element. * * @return returns formatted price or <code>NULL</code> if price is not found. */ private String getPrice(Element priceElement) { return priceElement == null ? null : priceElement.getChildText("FormattedPrice"); } /** * Adds image to item. Image details are taken from item element. * * @param item item to add image to. * @param itemElement item element. */ private void addImagesToItem(AmazonItem item, Element itemElement) { item.setSmallImage(getImage(itemElement, "SmallImage")); item.setMediumImage(getImage(itemElement, "MediumImage")); item.setLargeImage(getImage(itemElement, "LargeImage")); } /** * Converts image element to details object. * * @param itemElement item element to get image element from. * @param imageElementName image element name. * * @return image details or <code>NULL</code> if element is not present. */ private AmazonImageDetails getImage(Element itemElement, String imageElementName) { AmazonImageDetails imageDetails = null; Element imageElement = itemElement.getChild(imageElementName); if (imageElement != null) { String urlString = imageElement.getChildText("URL"); String heightString = imageElement.getChildText("Height"); String widthString = imageElement.getChildText("Width"); try { URL url = new URL(urlString); int height = Integer.parseInt(heightString); int width = Integer.parseInt(widthString); imageDetails = new AmazonImageDetails(url, width, height); } catch (MalformedURLException e) { LOG.log(Level.WARNING, MessageFormat.format( Strings.error("invalid.url"), urlString), e); } catch (NumberFormatException e) { LOG.warning(MessageFormat.format( Strings.error("amazon.invalid.image.dimensions"), widthString, heightString)); } } return imageDetails; } /** * Sleeps between calls to AWS to follow the call frequency contract. */ private static void sleepBetweenCalls() { try { Thread.sleep(TIME_BETWEEN_CALLS_MS); } catch (InterruptedException e) { LOG.warning(Strings.error("interrupted")); } } }