/*
* [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 );
}
}
}
}