/* * [AddBindings.java] * * Summary: Add EANs of alternate bindings including Kindle to existing books. * * Copyright: (c) 2012-2017 Roedy Green, Canadian Mind Products, http://mindprod.com * * Licence: This software may be copied and used freely for any purpose but military. * http://mindprod.com/contact/nonmil.html * * Requires: JDK 1.8+ * * Created with: JetBrains IntelliJ IDEA IDE http://www.jetbrains.com/idea/ * * Version History: * 1.0 2012-03-14 initial version */ package com.mindprod.aws; import com.mindprod.aws.jax.AWSECommerceService; import com.mindprod.aws.jax.AWSECommerceServicePortType; import com.mindprod.aws.jax.AwsHandlerResolver; import com.mindprod.aws.jax.Item; import com.mindprod.aws.jax.ItemAttributes; import com.mindprod.aws.jax.ItemLookup; import com.mindprod.aws.jax.ItemLookupRequest; import com.mindprod.aws.jax.Items; import com.mindprod.aws.jax.OperationRequest; import com.mindprod.common18.Build; import com.mindprod.common18.EIO; import com.mindprod.common18.Misc; import com.mindprod.common18.Progress; import com.mindprod.common18.ST; import com.mindprod.hunkio.HunkIO; import com.mindprod.isbn.ISBNValidate; import com.mindprod.stores.BookMacroFileFilter; import com.mindprod.stores.ManipulateMacros; import javax.xml.namespace.QName; import javax.xml.ws.Holder; import javax.xml.ws.WebServiceException; import java.io.File; import java.io.IOException; import java.net.MalformedURLException; import java.net.URL; import java.util.List; import static java.lang.System.*; /** * Add EANs of alternate bindings including Kindle to existing books. *

* It does not get info about whether the books are in stock. * Needs unlimited key JCE extension. * * @author Roedy Green, Canadian Mind Products * @version 1.0 2012-03-14 initial version * @see Binding for binary categories * @since 2012-03-14 */ public class AddBindings extends ManipulateMacros { /** * true if debugging, and want extra output */ private static final boolean DEBUGGING = true; /** * true if probe amazon.ca , false amazon.com. Surprisingly this is only referenced twice in the program. */ private static final boolean IS_CANADIAN = false; /** * fetch public amazon.com-assigned associateTag from environment set awsassociatetag=xxxxx */ private static final String ASSOCIATE_TAG = IS_CANADIAN ? System.getenv( "awsassociatetagca" ) : System.getenv( "awsassociatetag" ); /** * how many milliseconds to stall between probes */ private static final long STALL = 2000; /** * fetch confidential amazon.com-assigned awsAccessKeyId from environment set awsaccesskeyid=xxxxx */ private static final String AWS_ACCESS_KEY_ID = System.getenv( "awsaccesskeyid" ); /** * fetch confidential amazon.com-assigned awsSecretAccessKey assigned by Amazon set awssecretaccesskey=xxxxx */ private static final String AWS_SECRET_ACCESS_KEY = System.getenv( "awssecretaccesskey" ); /** * Namespace for amazon.com in the USA. Would have to be modified for other countries. */ private static final String NAME_SPACE_URI = "http://webservices.amazon.com/AWSECommerceService/2013-08-01"; /** * name of the SOAP service */ private static final String QNAME = "AWSECommerceService"; /** * location of JAX schmo, locally cached. for amazon.com in the USA. Would have to be modified for other countries. * Original came from http://ecs.amazonaws.com/AWSECommerceService/AWSECommerceService.wsd */ private static final String WSDL_LOCATION = "file:///E:/com/mindprod/aws/jax/AWSECommerceService.wsdl"; /** * where Book files are kept */ private static File bookDir; private static String originalContents; /** * audio EAN */ private static String audio; /** * hardcover EAN */ private static String hardcover; /** * primary EAN */ private static String isbn13; /** * primaryAsin of kindle */ private static String kindle; /** * EAN of paperback */ private static String paperback; /** * primary primaryAsin */ private static String primaryAsin; /** * Convert primaryAsin back to EAN isbn-13 * * @param asin amazon primaryAsin number of book. If EAN already returns EAN. * * @return isbn13 * @throws java.net.MalformedURLException if generated URL is malformed. */ private static String asinToEAN( final String asin ) throws MalformedURLException { if ( asin == null || asin.length() == 13 ) { return asin; // actually EAN already } // define magic configuring strings final AWSECommerceServicePortType port = getPort(); // new ItemLookup final ItemLookup itemLookup = new ItemLookup(); // new ItemLookupRequest which is part of the ItemLookup final ItemLookupRequest itemLookupRequest = new ItemLookupRequest(); // odd our lookup request to the the Lookup. final List itemLookupRequests = itemLookup.getRequest(); itemLookupRequests.add( itemLookupRequest ); // Set up the values of the ItemLookupRequest itemLookupRequest.setIdType( "ASIN" ); itemLookupRequest.getItemId().add( asin ); // there is no setItemID method // specify info to include in response final List responseGroup = itemLookupRequest.getResponseGroup(); responseGroup.add( "ItemAttributes" ); // set up Holder for the response tree final Holder operationRequest = new Holder<>(); final Holder> items = new Holder<>(); final String marketplaceDomain = ""; // not yet supported final String xmlEscaping = "Single"; final String validate = ""; // Probe Amazon Server, WARNING! Amazon changes the parms to this method frequently. // Inserts awsAccessKeyID and associateTag // note order validate, xmlEscaping, different from itemSearch port.itemLookup( marketplaceDomain, AWS_ACCESS_KEY_ID, ASSOCIATE_TAG, validate, xmlEscaping, itemLookupRequest, itemLookupRequests, operationRequest, items ); // analyse results final List result = items.value; final int size = result.get( 0 ).getItem().size(); for ( int i = 0; i < size; i++ ) { final Item item = result.get( 0 ).getItem().get( i ); final ItemAttributes attributes = item.getItemAttributes(); if ( attributes != null ) { return attributes.getEAN(); } } err.println( "cannot find EAN corresponding to " + asin ); return null; } /** * clear to prepare for another book */ private static void clear() { audio = null; hardcover = null; kindle = null; paperback = null; primaryAsin = null; } /** * standard boilerplate to connect to amazon.com AWS server with SOAP * * @return port to use to send requests. * @throws java.net.MalformedURLException if some URLs were mis-specified in the following code. */ private static AWSECommerceServicePortType getPort() throws MalformedURLException { // Set the service: AWSECommerceService service = new AWSECommerceService( new URL( WSDL_LOCATION ), new QName( NAME_SPACE_URI, QNAME ) ); // AwsHandlerResolver does the timestamp, signing and Base64 encoding service.setHandlerResolver( new AwsHandlerResolver( AWS_SECRET_ACCESS_KEY ) ); // Set the service port: return IS_CANADIAN ? service.getAWSECommerceServicePortCA() : service.getAWSECommerceServicePort(); } /** * insert new bindings we discovered. * * @param file file where insert new info * * @throws java.io.IOException if trouble writing file */ private static void insertNewBindings( final File file ) throws IOException { // hardcover, paperback kindle and audio set. null if no new ISBNs. try { // does nothing unless values are defined String newContents = appendToCommaList( originalContents, "Book", "paperback", paperback, file ); newContents = appendToCommaList( newContents, "Book", "hardcover", hardcover, file ); newContents = appendToCommaList( newContents, "Book", "kindle", kindle, file ); try { if ( audio != null ) { ISBNValidate.ensureISBN13Valid( audio ); newContents = appendToCommaList( newContents, "Book", "audio", audio, file ); } } catch ( IllegalArgumentException e ) { err.println( "Warning audio " + audio + " dropped. Not valid ISBN." ); } if ( !newContents.equals( originalContents ) ) { if ( DEBUGGING ) { // write corrections to *.bak file for examination. HunkIO.writeEntireFile( new File( EIO.getCanOrAbsPath( file ) + ".bak" ), newContents, HunkIO.UTF8 ); } else { HunkIO.writeEntireFile( file, newContents, HunkIO.UTF8 ); } } } catch ( IllegalArgumentException e ) { err.println( "Error in file " + EIO.getCanOrAbsPath( file ) + " : " + e.getMessage() ); } } /** * Probe the amazon AWS api for isbn13, results in class vars * * @throws java.net.MalformedURLException if generated URL is malformed. */ private static void queryISBN() throws MalformedURLException, WebServiceException { // for amazon.com in USA // define magic configuring strings final AWSECommerceServicePortType port = getPort(); // new ItemLookup final ItemLookup itemLookup = new ItemLookup(); // new ItemLookupRequest which is part of the ItemLookup final ItemLookupRequest itemLookupRequest = new ItemLookupRequest(); // odd our lookup request to the the Lookup. final List itemLookupRequests = itemLookup.getRequest(); itemLookupRequests.add( itemLookupRequest ); // Set up the values of the ItemLookupRequest itemLookupRequest.setSearchIndex( "Books" ); itemLookupRequest.setIdType( "EAN" ); itemLookupRequest.getItemId().add( isbn13 ); // there is no setItemID method // specify info to include in response final List responseGroup = itemLookupRequest.getResponseGroup(); responseGroup.add( "Images" ); responseGroup.add( "ItemAttributes" ); // set up Holder for the response tree final Holder operationRequest = new Holder<>(); final Holder> items = new Holder<>(); final String marketplaceDomain = ""; // not yet supported final String xmlEscaping = "Single"; final String validate = ""; // Probe Amazon Server, WARNING! Amazon changes the parms to this method frequently. // Inserts awsAccessKeyID and associateTag // note order validate, xmlEscaping, different from itemSearch port.itemLookup( marketplaceDomain, AWS_ACCESS_KEY_ID, ASSOCIATE_TAG, validate, xmlEscaping, itemLookupRequest, itemLookupRequests, operationRequest, items ); // might throw ClientTransportException/WebServiceException // analyse results final List result = items.value; final int size = result.get( 0 ).getItem().size(); for ( int i = 0; i < size; i++ ) { final Item item = result.get( 0 ).getItem().get( i ); primaryAsin = item.getASIN(); final ItemAttributes attributes = item.getItemAttributes(); final String binding = attributes.getBinding(); if ( binding != null ) { if ( binding.equals( "Paperback" ) ) { paperback = isbn13; } else if ( binding.equals( "Hardcover" ) ) { hardcover = isbn13; } } } //end loop to process results } /** * probe the amazon AWS api to find alternate bindings. * * @throws java.net.MalformedURLException if generated URL is malformed */ private static void queryOtherBindings() throws MalformedURLException { final AWSECommerceServicePortType port = getPort(); // new ItemLookup final ItemLookup itemLookup = new ItemLookup(); // new ItemLookupRequest which is part of the ItemLookup final ItemLookupRequest itemLookupRequest = new ItemLookupRequest(); // odd our lookup request to the the Lookup. final List itemLookupRequests = itemLookup.getRequest(); itemLookupRequests.add( itemLookupRequest ); // Set up the values of the ItemLookupRequest // lookup of variants only works with ASIN itemLookupRequest.setIdType( "ASIN" ); itemLookupRequest.getItemId().add( primaryAsin ); // there is no setItemID method // specify info to include in response final List responseGroup = itemLookupRequest.getResponseGroup(); responseGroup.add( "AlternateVersions" ); // set up Holder for the response tree final Holder operationRequest = new Holder<>(); final Holder> items = new Holder<>(); final String marketplaceDomain = ""; // not yet supported final String xmlEscaping = "Single"; final String validate = ""; // Probe Amazon Server, WARNING! Amazon changes the parms to this method frequently. // Inserts awsAccessKeyID and associateTag // note order validate, xmlEscaping, different from itemSearch port.itemLookup( marketplaceDomain, AWS_ACCESS_KEY_ID, ASSOCIATE_TAG, validate, xmlEscaping, itemLookupRequest, itemLookupRequests, operationRequest, items ); // analyse results final List result = items.value; final int size = result.get( 0 ).getItem().size(); for ( int i = 0; i < size; i++ ) { final Item item = result.get( 0 ).getItem().get( i ); Item.AlternateVersions alternateVersions = item.getAlternateVersions(); if ( alternateVersions != null ) { for ( Item.AlternateVersions.AlternateVersion alternateVersion : alternateVersions .getAlternateVersion() ) { final String asin = alternateVersion.getASIN(); final String bindingName = alternateVersion.getBinding(); final Binding b = Binding.exactMatch( bindingName ); recordBindingASIN( b, asin ); } // end loop to process alternateVersions pass 1 // could not get a precise match, go for an approx match. for ( Item.AlternateVersions.AlternateVersion alternateVersion : alternateVersions .getAlternateVersion() ) { final String asin = alternateVersion.getASIN(); final String bindingName = alternateVersion.getBinding(); final Binding b = Binding.approximateMatch( bindingName ); if ( b == null ) { err.println( "Unrecognised binding " + bindingName + " on asin:" + asin ); } else { recordBindingASIN( b, asin ); } } // end loop to process alternateVersions pass 2 } // end alternateVersions not null } //end loop to process results paperback = asinToEAN( paperback ); hardcover = asinToEAN( hardcover ); audio = asinToEAN( audio ); } /** * record the asin in the corresponding binding variable, if we don't have a value already. * * @param b Binding enum matching description from Amazon * @param asin Amazon part number. */ private static void recordBindingASIN( final Binding b, final String asin ) { if ( b == null ) { return; } switch ( b ) { case PAPERBACK: if ( paperback == null ) { // first one paperback = asin; } break; case HARDCOVER: if ( hardcover == null ) { hardcover = asin; } break; case AUDIO: if ( audio == null ) { audio = asin; } break; case KINDLE: if ( kindle == null ) { kindle = asin; } break; case EBOOK: case WEBBOOK: case OTHER: default: // ignore } } /** * add alternate bindings to existing book macros. * Does not remove bindings. Even if amazon.com does not stock them, some other store might. * * @param args not used * * @throws java.io.IOException if trouble reading/writing files * @see FetchBookFacts */ public static void main( String[] args ) throws IOException { final File webrootDir = new File( Build.MINDPROD_WEBROOT ); bookDir = new File( webrootDir, "book" ); if ( DEBUGGING ) { out.println( "ASSOCIATE_TAG = " + ASSOCIATE_TAG ); out.println( "AWS_ACCESS_KEY_ID = " + AWS_ACCESS_KEY_ID ); out.println( "AWS_SECRET_ACCESS_KEY = " + AWS_SECRET_ACCESS_KEY ); } if ( DEBUGGING ) { processOneFile( new File( "E:/mindprod/book/9781590595374.html" ) ); } processFiles(); Misc.trackLastThread(); System.exit( 0 ); } public static void processFiles() throws IOException { final String[] files = bookDir.list( new BookMacroFileFilter() ); final Progress progress = new Progress( files.length ); for ( String filename : files ) // /book BookHead macro files { try { Thread.sleep( STALL ); } catch ( InterruptedException e ) { } progress.progress( out ); final File file = new File( bookDir, filename ); isbn13 = filename.substring( 0, 13 ); processOneFile( file ); } } /** * probe AWS for new isbn for paperback, hardcover, kindle or audia. * * @param filename * @param file * * @throws IOException */ public static void processOneFile( final File file ) throws IOException { clear(); originalContents = HunkIO.readEntireFile( file, HunkIO.UTF8 ); final String oneMacro = findFirstMacroContents( originalContents, "Book" ); paperback = parseValueByKey( oneMacro, "paperback" ); hardcover = parseValueByKey( oneMacro, "hardcover" ); kindle = parseValueByKey( oneMacro, "kindle" ); audio = parseValueByKey( oneMacro, "audio" ); // ebook empty is ok. We can't fix it here. Need to fix in findNooks. if ( ( ST.isEmpty( paperback ) || ST.isEmpty( hardcover ) || ST.isEmpty( kindle ) || ST.isEmpty( audio ) ) ) { try { queryISBN(); queryOtherBindings(); insertNewBindings( file ); } catch ( WebServiceException e ) { err.println( "web service probe failed for isbn: " + isbn13 ); } catch ( MalformedURLException e ) { err.println( "bad url for isbn: " + isbn13 ); } } } }