Home | FAQ | Contact me

Abstract class—yet another illustration

This is how to implement abstraction for the purpose of folding common code into an abstract class that nevertheless needs to work with different data depending on the extending classes.

Our example is a symbol table.

SymbolTable.java:

package com.etretatlogiciels.utilities;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;

/**
 * Symple symbol table.
 * @author Russell Bateman
 * @since May 2015
 */
public abstract class SymbolTable< K, V >
{
  protected Map< K, V > table;

  /**
   * Puts a new symbol in the table.
   * @param key new symbol to put into table.
   * @param value of the key/symbol.
   * @throws SymbolTableException if key already exists.
   */
  public void put( K key, V value ) throws SymbolTableException
  {
    if( table.containsKey(  key ) )
      throw new SymbolTableException( "Key already exists" );

    table.put( key, value );
  }

  public V get( K key ) throws SymbolTableException
  {
    if( !table.containsKey( key ) )
      throw new SymbolTableException( "Key does not exist" );

    return table.get( key );
  }

  /**
   * Remove symbol from table.
   * @param key symbol to remove.
   * @throws SymbolTableException if key does not exist.
   */
  public void delete( K key ) throws SymbolTableException
  {
    if( !table.containsKey( key ) )
      throw new SymbolTableException( "Key does not exist" );

    table.remove( key );
  }

  /**
   * Symbol does or does not exist in table.
   * @param key
   * @return true/false that symbol exists.
   */
  public boolean contains( K key )
  {
    return table.containsKey( key );
  }

  /**
   * Symbol table is or isn't empty.
   * @return true/false that the symbol table is empty.
   */
  public boolean isEmpty()
  {
    return size() > 0;
  }

  /**
   * Number of symbols in table.
   * @return count of keys/symbols in the table.
   */
  public int size()
  {
    return table.size();
  }

  /**
   * Traverse this list to get all the keys; call get( String key )
   * for their corresponding values.
   *
   * @return unsorted list of keys in this table.
   */
  public List< K > keys()
  {
    List< K > keys = new ArrayList< K >( size() );

    for( Map.Entry< K, V > map : table.entrySet() )
      keys.add( map.getKey() );

    return keys;
  }

  /**
   * If you just have to have our map, here it is.
   * @return the symbol table map.
   */
  public Map< K, V > sortByKey()
  {
    Map< K, V > sortedMap  = new HashMap< K, V >();

    for( K key : keys() )
    {
      try
      {
        sortedMap.put( key, get( key ) );
      }
      catch( SymbolTableException e )
      {
        // can't happen unless consuming us from multiple threads...
      }
    }

    return sortedMap;
  }

  /**
   * Our magic, private, miraculous sort-by-value utility.
   * @param map the symbol table.
   * @return the table sorted by value.
   */
  protected static < K, V extends Comparable< ? super V > > Map< K, V > sortMapByValue( Map< K, V > map )
  {
    List< Map.Entry< K, V > > list = new LinkedList<>( map.entrySet() );
    Collections.sort( list, new Comparator< Map.Entry< K, V > >()
      {
        @Override
        public int compare( Entry< K, V > o1, Entry< K, V > o2 )
        {
          return ( o1.getValue() ).compareTo( ( V ) o2.getValue() );
        }
      }
    );

    Map< K, V > result = new LinkedHashMap<>();

    for( Map.Entry< K, V > entry : list )
      result.put( entry.getKey(), entry.getValue() );

    return result;
  }

  /**
   * If you want a LinkedHashMap, sorted by value, of our
   * table, use this.
   * @return the symbol table map, but sorted by value.
   */
  public abstract Map< K, V > sortedByValue();
}
OccurrencesTable.java:
package com.etretatlogiciels.utilities;

import java.util.Map;
import java.util.HashMap;

public class OccurrencesTable extends SymbolTable< String, Integer >
{
  public OccurrencesTable()
  {
    table = new HashMap< String, Integer >();
  }

  /**
   * If you want a LinkedHashMap, sorted by value, of our
   * table, use this.
   * @return the symbol table map, but sorted by value.
   */
  @Override
  public Map< String, Integer > sortedByValue()
  {
    return sortMapByValue( table );
  }
}
ParentageTable.java:
package com.etretatlogiciels.utilities;

import java.util.HashMap;
import java.util.Map;

public class ParentageTable extends SymbolTable< String, String >
{
  public ParentageTable()
  {
    table = new HashMap< String, String >();
  }

  /**
   * If you want a LinkedHashMap, sorted by value, of our
   * table, use this.
   * @return the symbol table map, but sorted by value.
   */
  @Override
  public Map< String, String > sortedByValue()
  {
    return sortMapByValue( table );
  }
}

CdaHandler.java:

Consuming the table(s).

package com.etretatlogiciels.cda.analysis;

import java.util.Collections;
import java.util.List;

import org.xml.sax.Attributes;
import org.xml.sax.SAXException;
import org.xml.sax.helpers.DefaultHandler;

import com.etretatlogiciels.utilities.RedirectSystemOutStream;
import com.etretatlogiciels.utilities.OccurrencesTable;
import com.etretatlogiciels.utilities.ParentageTable;
import com.etretatlogiciels.utilities.SymbolTableException;
import com.etretatlogiciels.utilities.XmlElementStack;

/**
 * We want to analyse elements, modifying our analysis to make a growing number of
 * observations about CDA and CCD documents.
 *
 * History
 *
 * We already gather out <text> ... </text> elements from some CDA
 * elements. What we want to do is identify other, import content to gather out
 * and extend our work to CCDs, which adhere to the CDA format standard.
 *
 * About this file...
 *
 * The SAX handler is the cookie jar with all the cookies. It's the code that says
 * what we're looking for and how it's supposed to show up. In this case, however,
 * we don't want to make many decisions about what's going to be in the CDA (or
 * CCD) because we're just learning. And, anyway, we likely would never be able to
 * predict with complete accuracy what's in there. We're just observers trying to
 * find a way to disembowel the document to get content out.
 *
 * @author Russell Bateman
 * @since May 2015
 */
public class CdaHandler extends DefaultHandler
{
  private String           elementName;
  private String           characters;
  private OccurrencesTable occurrences;
  private ParentageTable   parentage;
  private XmlElementStack  stack;

  private static final String DOCUMENT = "<document-top>";

  /**
   * Just output to the console whatever's found.
   */
  public CdaHandler()
  {
    super();
    occurrences = new OccurrencesTable();
    stack       = new XmlElementStack();
  }

  /**
   * Hereafter, all output goes to System.out which will take it to a file and/or
   * the console.
   * @param outputFilepath to which results will go.
   * @param hush shut the console up (no output to console).
   */
  public CdaHandler( String outputFilepath, boolean hush )
  {
    this();
    RedirectSystemOutStream.redirectSystemOutToFile( outputFilepath, hush );
  }

  /**
   * If processing multiple documents, this would be an opportunity to initialize
   * processing for the next document (as opposed to a previous one).
   */
  public void startDocument() throws SAXException
  {
    stack.push( DOCUMENT );
  }

  /**
   * If processing multiple documents, this is an opportunity to tie off the one
   * that's been processing and is finished.
   */
  public void endDocument() throws SAXException
  {
    List< String > elements = occurrences.keys();
    int            widest   = CdaAnalysisUtilities.getMaximumKeyWidth( elements );

    // Sort alphabetically by key...
    Collections.sort( elements );
    CdaAnalysisUtilities.printPaddedList( occurrences, elements, widest );

    System.out.println( "-----------------------------------------------------------------------" );
    // Sorted by occurrence from most to least often...
    CdaAnalysisUtilities.sortAndPrintPaddedByValue( occurrences, elements, widest );

    if( stack.peek().equals( DOCUMENT ) )
        System.out.println( "Document is well formed" );
  }

  /**
   * —what identifies and handles the opening element tag. These can be
   * intercepted and used for analysis.
   */
  public void startElement( String uri, String localName, String qName, Attributes attributes ) throws SAXException
  {
    String elementName = qName;

    stack.push( elementName );

    if( !occurrences.contains( elementName ) )
    {
      try
      {
        occurrences.put( elementName, 1 );
      }
      catch( SymbolTableException e )
      {
        e.printStackTrace();
      }
    }
    else
    {
      try
      {
        Integer count = occurrences.get( elementName );
        count++;
        occurrences.delete( elementName );
        occurrences.put( elementName, count );
      }
      catch( SymbolTableException e )
      {
        e.printStackTrace();
      }
    }

//    System.out.println( "<" + qName + ">" );
  }

  /**
   * —what identifies the closing element tag, an opportunity to wrap up
   * the greater tag, whatever we want to do with it. It's at this point that
   * we know the tag is finished and what we've got in characters is its content.
   */
  public void endElement( String uri, String localName, String qName ) throws SAXException
  {
  }

  /**
   * —what gathers the plain text content between the opening and closing tags.
   * Of course, some elements are comprehensive and contain additional elements without
   * also having plain text.
   */
  public void characters( char ch[], int start, int length ) throws SAXException
  {
    characters = new String( ch, start, length ).trim();

//    if( characters.length() > 0 )
//      System.out.println( characters );
  }

  public void ignorableWhitespace( char ch[], int start, int length ) throws SAXException
  {
    @SuppressWarnings( "unused" )
    String whitespace = new String( ch, start, length ).trim();
  }
}