Monday, June 1, 2009

HtmlUnit: listening to their customers

I'm delighted to say I received an e-mail from the HtmlUnit team this morning that a couple of the requests I made have been incorporated into their next build. Thanks guys! Such prompt attention and turn-around really reinforces my confidence in the decision to switch.

They've added getAnchorByText and getOptionByText. This means I can remove some of the code from my ad hoc HtmlUnitUtils class. Of course, there are still more it'd be nice to see. I include here the whole of my utils class so that they may pick away at it for anything else they may want to incorporate.

Naturally I don't expect it all, or even most. But whatever they may add is awesome as it means less code for me to maintain!

package com.kennardconsulting.core.util;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.util.Arrays;
import java.util.List;

import org.metawidget.util.CollectionUtils;
import org.metawidget.util.simple.StringUtils;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

import com.gargoylesoftware.htmlunit.Page;
import com.gargoylesoftware.htmlunit.WebWindow;
import com.gargoylesoftware.htmlunit.html.HtmlAnchor;
import com.gargoylesoftware.htmlunit.html.HtmlElement;
import com.gargoylesoftware.htmlunit.html.HtmlFileInput;
import com.gargoylesoftware.htmlunit.html.HtmlForm;
import com.gargoylesoftware.htmlunit.html.HtmlOption;
import com.gargoylesoftware.htmlunit.html.HtmlPage;
import com.gargoylesoftware.htmlunit.html.HtmlRadioButtonInput;
import com.gargoylesoftware.htmlunit.html.HtmlSelect;
import com.gargoylesoftware.htmlunit.html.HtmlTable;
import com.kennardconsulting.core.enumeration.FileFormat;

/**
* Utilities for working with HtmlUnit.
*/

public final class HtmlUnitUtils
{
   //
   // Public statics
   //

   public static HtmlAnchor getLink( HtmlPage response, String text )
   {
      List<HtmlAnchor> links = getLinks( response, text );

      if ( links.isEmpty() )
         throw new RuntimeException( "No such link with exact text of '" + text + "'" );

      if ( links.size() > 1 )
         throw new RuntimeException( "More than one link with exact text of '" + text + "'" );

      return links.get( 0 );
   }

   public static List<HtmlAnchor> getLinks( HtmlPage response, String text )
   {
      return getLinks( response, text, false );
   }

   public static List<HtmlAnchor> getLinks( HtmlPage response, String text, boolean contains )
   {
      List<HtmlAnchor> anchors = CollectionUtils.newArrayList();

      for ( HtmlAnchor anchor : response.getAnchors() )
      {
         String anchorText = anchor.asText();
         anchorText = anchorText.replaceAll( "\r", "" );

         if ( contains )
         {
            if ( !anchorText.contains( text ) )
               continue;
         }
         else
         {
            if ( !anchorText.equals( text ) )
               continue;
         }

         anchors.add( anchor );
      }

      return anchors;
   }

   @SuppressWarnings( "unchecked" )
   public static <T extends HtmlElement> T getInputByNameEndingWith( HtmlForm form, String nameEndingWith )
   {
      for ( HtmlElement element : form.getHtmlElementsByTagName( "input" ) )
      {
         String elementName = element.getAttribute( "name" );

         if ( elementName == null )
            continue;

         if ( elementName.endsWith( nameEndingWith ) )
            return (T) element;
      }

      return null;
   }

   public static HtmlOption getSelectedOption( HtmlForm form, String selectName )
   {
      return getSelectedOption( form.getSelectByName( selectName ) );
   }

   public static HtmlOption getSelectedOption( HtmlSelect select )
   {
      List<HtmlOption> selectedOptions = select.getSelectedOptions();

      if ( selectedOptions.isEmpty() )
         return null;

      if ( selectedOptions.size() > 1 )
         throw new RuntimeException( "'" + select.getNameAttribute() + "' has more than one option selected" );

      return selectedOptions.get( 0 );
   }

   public static String setSelectedOption( HtmlForm form, String selectName, String option )
   {
      return setSelectedOption( form.getSelectByName( selectName ), option );
   }

   public static String setSelectedOption( HtmlForm form, String selectName, String option, boolean allowOnlyOne )
   {
      return setSelectedOption( form.getSelectByName( selectName ), option, allowOnlyOne );
   }

   public static String setSelectedOption( HtmlSelect select, String option )
   {
      return setSelectedOption( select, option, true );
   }

   public static String setSelectedOption( HtmlSelect select, String option, boolean allowOnlyOne )
   {
      String selectedValue = null;

      for ( HtmlOption htmlOption : select.getOptions() )
      {
         String htmlOptionText = htmlOption.asText();

         // Special support for trimming off  , which we use for indenting select options

         htmlOptionText = htmlOptionText.trim();

         if ( htmlOptionText.equals( option ) )
         {
            if ( selectedValue != null )
               throw new RuntimeException( "Select '" + select.getNameAttribute() + "' has more than one '" + option + "'" );

            selectedValue = htmlOption.getValueAttribute();
            htmlOption.setSelected( true );

            if ( !allowOnlyOne )
               break;
         }
      }

      if ( selectedValue == null )
         throw new RuntimeException( "Select '" + select.getNameAttribute() + "' does not contain '" + option + "'" );

      return selectedValue;
   }

   public static String setSelectedOptionValue( HtmlForm form, String selectName, String option )
   {
      return setSelectedOptionValue( form.getSelectByName( selectName ), option );
   }

   public static String setSelectedOptionValue( HtmlSelect select, String optionValue )
   {
      String selectedValue = null;

      for ( HtmlOption htmlOption : select.getOptions() )
      {
         if ( htmlOption.getValueAttribute().equals( optionValue ) )
         {
            if ( selectedValue != null )
               throw new RuntimeException( "Select '" + select.getNameAttribute() + "' has more than one '" + optionValue + "'" );

            selectedValue = htmlOption.getValueAttribute();
            htmlOption.setSelected( true );
         }
      }

      if ( selectedValue == null )
         throw new RuntimeException( "Select '" + select.getNameAttribute() + "' does not contain value '" + optionValue + "'" );

      return selectedValue;
   }

   public static boolean hasOption( HtmlForm form, String selectName, boolean selected, String... options )
   {
      int found = 0;

      for ( HtmlOption htmlOption : form.getSelectByName( selectName ).getOptions() )
      {
         for ( String option : options )
         {
            if ( htmlOption.asText().equals( option ) )
            {
               if ( selected && !htmlOption.isSelected() )
                  return false;

               found++;
               break;
            }
         }
      }

      return ( found == options.length );
   }

   public static boolean hasOptionValue( HtmlForm form, String selectName, boolean selected, String... options )
   {
      int found = 0;

      for ( HtmlOption htmlOption : form.getSelectByName( selectName ).getOptions() )
      {
         for ( String option : options )
         {
            if ( htmlOption.getValueAttribute().equals( option ) )
            {
               if ( selected && !htmlOption.isSelected() )
                  return false;

               found++;
               break;
            }
         }
      }

      return ( found == options.length );
   }

   public static String getOptionValue( HtmlForm form, String selectName, String option )
   {
      for ( HtmlOption htmlOption : form.getSelectByName( selectName ).getOptions() )
      {
         if ( htmlOption.asText().equals( option ) )
            return htmlOption.getValueAttribute();
      }

      throw new RuntimeException( "No option with text '" + option + "' found" );
   }

   public static void setSelectedRadio( HtmlForm form, String radioName, String value )
   {
      boolean selectedOne = false;

      for ( HtmlRadioButtonInput htmlRadioButtonInput : form.getRadioButtonsByName( radioName ) )
      {
         if ( !htmlRadioButtonInput.getValueAttribute().trim().equals( value ) )
            continue;

         if ( selectedOne )
            throw new RuntimeException( "Radio button group '" + radioName + "' has more than one '" + value + "'" );

         selectedOne = true;
         htmlRadioButtonInput.setChecked( true );
      }

      if ( !selectedOne )
         throw new RuntimeException( "Radio button group '" + radioName + "' has no option '" + value + "'" );
   }

   public static boolean hasRadio( HtmlForm form, String radioName, String value )
   {
      for ( HtmlRadioButtonInput htmlRadioButtonInput : form.getRadioButtonsByName( radioName ) )
      {
         if ( htmlRadioButtonInput.getValueAttribute().trim().equals( value ) )
            return true;
      }

      return false;
   }

   public static String getSelectedRadioValue( HtmlForm form, String radioName )
   {
      HtmlRadioButtonInput radioButtonInputSelected = null;

      for ( HtmlRadioButtonInput htmlRadioButtonInput : form.getRadioButtonsByName( radioName ) )
      {
         if ( !htmlRadioButtonInput.isChecked() )
            continue;

         if ( radioButtonInputSelected != null )
            throw new RuntimeException( "Radio button group '" + radioName + "' has more than one selected" );

         radioButtonInputSelected = htmlRadioButtonInput;
      }

      if ( radioButtonInputSelected == null )
         return null;

      return radioButtonInputSelected.getValueAttribute();
   }

   @SuppressWarnings( "unchecked" )
   public static <E extends HtmlElement> E getElementByAttribute( HtmlPage page, String elementName, String attributeName, String attributeValue )
   {
      return (E) getElementByAttribute( page.getDocumentElement(), elementName, attributeName, attributeValue );
   }

   public static <E extends HtmlElement> E getElementByAttribute( HtmlElement element, String elementName, String attributeName, String attributeValue )
   {
      List<E> elements = element.getElementsByAttribute( elementName, attributeName, attributeValue );

      if ( elements.isEmpty() )
         return null;

      if ( elements.size() > 1 )
         throw new RuntimeException( "More than one " + elementName + " with " + attributeName + " of '" + attributeValue + "'" );

      return elements.get( 0 );
   }

   public static <E extends HtmlElement> E getElementByAttributeContaining( HtmlPage page, String elementName, String attributeName, String attributeValueContained )
   {
      List<E> elements = getElementsByAttributeContaining( page, elementName, attributeName, attributeValueContained );

      if ( elements.isEmpty() )
         return null;

      if ( elements.size() > 1 )
         throw new RuntimeException( "More than one " + elementName + " with " + attributeName + " containing '" + attributeValueContained + "': " + CollectionUtils.toString( elements ) );

      return elements.get( 0 );
   }

   /**
    * @return the elements, in the order they are declared in the HTML.
    */

   @SuppressWarnings( "unchecked" )
   public static <E extends HtmlElement> List<E> getElementsByAttributeContaining( HtmlPage page, String elementName, String attributeName, String attributeValueContained )
   {
      List<E> toReturn = CollectionUtils.newArrayList();

      NodeList nodeList = page.getElementsByTagName( elementName );

      for ( int loop = 0, length = nodeList.getLength(); loop < length; loop++ )
      {
         Node node = nodeList.item( loop );
         Node nodeValue = node.getAttributes().getNamedItem( attributeName );

         if ( nodeValue == null )
            continue;

         if ( nodeValue.getNodeValue().contains( attributeValueContained ) )
            toReturn.add( (E) node );
      }

      return toReturn;
   }

   @SuppressWarnings("unchecked")
   public static <T extends Page> T waitForAjax( T page )
   {
      WebWindow window = page.getEnclosingWindow();
      window.getThreadManager().joinAll( 10000 );

      return (T) window.getEnclosedPage();
   }

   public static void setUpload( HtmlForm form, String uploadName, String url )
   {
      setUpload( form, uploadName, CoreStringUtils.substringAfterLast( url, StringUtils.SEPARATOR_FORWARD_SLASH ), url );
   }

   public static void setUpload( HtmlForm form, String uploadName, String name, String url )
   {
      try
      {
         setUpload( form, uploadName, name, new URL( url ).openStream() );
      }
      catch ( IOException e )
      {
         throw new RuntimeException( e );
      }
   }

   public static void setUpload( HtmlForm form, String uploadName, String name, InputStream streamIn )
   {
      HtmlFileInput fileInput = form.getInputByName( uploadName );

      ByteArrayOutputStream streamOut = new ByteArrayOutputStream();

      try
      {
         IOUtils.streamBetween( streamIn, streamOut );
      }
      catch ( IOException e )
      {
         throw new RuntimeException( e );
      }

      fileInput.setValueAttribute( name );
      fileInput.setData( streamOut.toByteArray() );
   }

   public static HtmlTable getTableStartingWith( HtmlPage page, String startingWith )
   {
      NodeList tables = page.getElementsByTagName( "table" );

      for( int loop = 0, length = tables.getLength(); loop < length; loop++ )
      {
         HtmlTable table = (HtmlTable) tables.item( loop );

         if ( table.asText().trim().startsWith( startingWith ))
            return table;
      }

      throw new RuntimeException( "No table starting with '" + startingWith + "'" );
   }

   //
   // Private constructor
   //

   private HtmlUnitUtils()
   {
      // Can never be called
   }
}

1 comments:

asafp said...

Thank you Kennard Consulting. The HtmlUnitUtils saved me some time.