# Martin Fowler's Reader Framework example from his "Language Workbenches"
# and "Generating Code for DSLs" papers.  
# H. Conrad Cunningham 
# Original:  9 October 2006
# Revision: 10 October 2006

# READING AN EXTERNAL TEXT DSL USING A TWO-PASS PARSER

require 'ReaderFramework'
require 'ReaderUtilities'

# This program implements a two-pass parser and builder for the
# ReaderFramework::Reader objects.  The first pass parses the concrete
# syntax of the DSL to produce an Abstract Syntax Tree (AST).  The second
# pass takes the AST and configures the Reader.  The advantage of this
# approach is that it allows the two passes to be varied independently as
# long as the abstract syntax remains the same.

# FIRST PASS: PARSER FOR TEXT DSL DESCRIPTION

# Class BuilderParserText encapsulates the first pass of the two-pass
# builder. It takes a filename "dslfile", reads the text DSL description,
# and generates the abstract syntax tree.

# Example DSL text description input:
#   mapping SVCL dsl.ServiceCall
#     4-18: CustomerName
#     19-23: CustomerID
#     24-27 : CallTypeCode
#     28-35 : DateOfCallString
# 
#   mapping  USGE dsl.Usage
#     4-8 : CustomerID
#     9-22: CustomerName
#     30-30: Cycle
#     31-36: ReadDate

# Method BuilderParserText#configure initiates the pass.

class BuilderParserText

  include ReaderUtilities

  attr_reader :configuration

  # takes text DSL description file name
  def initialize(dslfile)
    @dslfile = dslfile
    @configuration = ReaderConfiguration.new
  end

  def configure
    input = File.new(@dslfile)
    input.each do |line|
      process_line(line)
    end
    input.close
  end

private

  def process_line(line)
    trimmed = line.rstrip
    return if is_blank? trimmed
    return if is_comment? trimmed
    if is_new_mapping? trimmed
      make_mapping(trimmed)
    else 
      make_field(trimmed)
    end
  end

  def is_new_mapping?(line)
    line =~ /\s*mapping/
  end

  def make_mapping(line)
    @current_mapping = Mapping.new
    tokens = line.scan(/\S+/)
    @current_mapping.code         = tokens[1].strip
    @current_mapping.target_class = tokens[2].strip
    @configuration.add_mapping(@current_mapping)
  end

  def make_field(line)
    f = Field.new()
    tokens = line.split(':')
    f.field_name = tokens[1].strip
    range_tokens = tokens[0].strip.split('-')
    f.begin_col = range_tokens[0].to_i
    f.end_col   = range_tokens[1].to_i
    @current_mapping.add_field(f)
  end

end#BuilderParserText


# FIRST PASS: PARSER FOR XML DSL

require 'rexml/document'

# Class BuilderParserXml encapsulates the first pass of the two-pass
# builder. It takes a filename "dslfile", reads the XML description of the
# DSL, and generates the abstract syntax tree.

# Example XML description of DSL:
#   <ReaderConfiguration>
#     <Mapping Code = "SVCL" TargetClass = "dsl.ServiceCall">
#       <Field name = "CustomerName" start = "4" end = "18"/>
#       <Field name = "CustomerID" start = "19" end = "23"/>
#       <Field name = "CallTypeCode" start = "24" end = "27"/>
#       <Field name = "DateOfCallString" start = "28" end = "35"/>
#     </Mapping> 
#    <Mapping Code = "USGE" TargetClass = "dsl.Usage">
#       <Field name = "CustomerID" start = "4" end = "8"/>
#       <Field name = "CustomerName" start = "9" end = "22"/>
#       <Field name = "Cycle" start = "30" end = "30"/>
#       <Field name = "ReadDate" start = "31" end = "36"/>
#     </Mapping>
#   </ReaderConfiguration>

# Method BuilderParserXml#configure initiates the pass.

class BuilderParserXml

  attr_accessor :configuration

  # takes XML DSL description file name
  def initialize(dslfile)
    @dslfile = dslfile
    @configuration = ReaderConfiguration.new
  end

  def configure
    input = File.new(@dslfile)
    doc = REXML::Document.new(input)
    doc.root.each_element do |mapping|
      process_mapping_node(mapping)
    end
    input.close
  end

private

  def process_mapping_node(mapping)
    @current_mapping = Mapping.new
    @configuration.add_mapping(@current_mapping)
    @current_mapping.code         = mapping.attributes["Code"]
    @current_mapping.target_class = mapping.attributes["TargetClass"]
    mapping.each_element do |field|
      @current_mapping.add_field(process_field_node(field))
    end
  end

  def process_field_node(field)
    result = Field.new
    result.field_name = field.attributes["name"]
    result.begin_col  = field.attributes["start"].to_i
    result.end_col    = field.attributes["end"].to_i
    result
  end

end#BuilderParserXml


# ABSTRACT SYNTAX TREE CLASSES

# The abstract syntax tree (AST) for the DSL has three levels. An instance
# of class ReaderConfiguration forms the root of the AST.  The second level
# of the AST is made up of instances of class Mapping.  The leaf level of
# the AST is made up of instances of class Field.

class ReaderConfiguration

  attr_reader :mappings

  def initialize
    @mappings = []
  end

  def add_mapping(mapping)
    @mappings << mapping
  end

end#ReaderConfiguration

class Mapping

  attr_accessor :code, :target_class
  attr_reader   :fields

  def initialize
    @fields = []
  end

  def add_field(field)
    @fields << field
  end

end#Mapping

class Field

  attr_accessor :begin_col, :end_col, :field_name

end#Field


# SECOND PASS: CONFIGURE READER DIRECTLY

# Class BuilderGenerator encapsulates the process for configuring the
# ReaderFramework::Reader given an abstract syntax tree generated by
# BuilderParserText or BuilderParserXml.  It takes a ReaderConfiguration
# containing the AST.

# Method BuilderGenerator#configure takes the Reader to be configured and
# configures the Reader according to the AST.

class BuilderGenerator

  include ReaderUtilities

  # takes AST and class name mapping hash
  def initialize(configuration)
    @configuration = configuration
  end

  def configure(reader)
    @configuration.mappings.each {|mapping| make_strategy(reader, mapping)}
  end

private
    
  def make_strategy(reader, mapping)
    strategy = ReaderFramework::ReaderStrategy.new(
                 mapping.code, class_name_only(mapping.target_class))
    reader.add_strategy(strategy)
    mapping.fields.each do |field| 
      strategy.add_field_extractor(field.begin_col, field.end_col, 
                                   field.field_name)
    end
  end

end#BuilderGenerator


# SECOND PASS:  BUILD EXTERNAL FILE OF RUBY CODE FOR EXECUTION

# Class BuilderGeneratorExternal is an ad hoc code generator for the code to
# configure the Reader Framework.  It takes an AST and a filename and
# generates a class named BuilderExternal and stores it in the filename.

# The template for the generated code is embedded in the write* methods of
# this class.  The code generated in BuilderExternal is similar to the code
# in the BuilderDirect class.

# Method BuilderGeneratorExternal#configure takes the Reader to be
# configured and configures the Reader according to the AST.

class BuilderGeneratorExternal

  include ReaderUtilities

  # takes AST and output file name for generated code
  def initialize(configuration, genfile)
    @configuration = configuration
    @genfile = genfile
  end

  def configure(reader)
    write_builder_code
    load_builder_code
    builder = BuilderExternal.new
    builder.configure(reader)
  end

private

  def load_builder_code
    rb_in = File.new(@genfile)
    BuilderGeneratorExternal.class_eval(rb_in.read, @genfile)
    rb_in.close
  end
    
  def write_builder_code
    @rout = File.new(@genfile,"w")
    write_class_header
    write_configure_header
    @configuration.mappings.each do |mapping| 
      write_mapping_add(mapping)
    end
    write_configure_trailer
    @configuration.mappings.each do |mapping|
      write_mapping_header(mapping)
      mapping.fields.each do |field|
        write_extractor(field)
      end
      write_mapping_trailer
    end
    write_class_trailer
    @rout.close
  end
  
  def write_class_header
    @rout.print "class BuilderExternal\n\n"
  end

  def write_class_trailer
    @rout.print "end\n\n"
  end

  def write_configure_header
    @rout.print "  def configure(reader)\n"
  end

  def write_configure_trailer
    @rout.print "  end\n\n"
  end

  def write_mapping_add(mapping)
    target = from_camel(class_name_only(mapping.target_class))
    @rout.print  "    reader.add_strategy(configure_#{target})\n"
  end

  def write_mapping_header(mapping)
    target_class = class_name_only(mapping.target_class)
    target = from_camel(target_class)
    @rout.print "  def configure_#{target}\n"
    prefix = "    result = ReaderFramework::ReaderStrategy.new("
    suffix = "\"#{mapping.code}\", #{target_class})"
    @rout.print "#{prefix}#{suffix}\n"
  end

  def write_mapping_trailer
    @rout.print "    result\n  end\n\n"
  end

  def write_extractor(field)
    prefix = "    result.add_field_extractor("
    range  = "#{field.begin_col.to_s}, #{field.end_col.to_s}"
    suffix = ", \"#{from_camel(field.field_name)}\")"
    @rout.print "#{prefix}#{range}#{suffix}\n"
  end

end#BuilderGeneratorExternal


# TWO-PASS BUILDER FOR TEXT DSL AND DIRECT CONFIGURATION

class ReaderBuilderTextTwoPass

  # takes text DSL file name and class name mapping hash
  def initialize(dslfile)
    @dslfile = dslfile
  end

  def configure(reader)
    @reader = reader
    parser = BuilderParserText.new(@dslfile)
    parser.configure
    builder_gen = BuilderGenerator.new(parser.configuration)
    builder_gen.configure(@reader)
  end

end#ReaderBuilderTextTwoPass


# TWO-PASS BUILDER FOR XML DSL AND DIRECT CONFIGURATION

class ReaderBuilderXmlTwoPass

  # takes XML DSL file and class name mapping hash
  def initialize(xmlfile)
    @xmlfile = xmlfile
  end

  def configure(reader)
    @reader = reader
    parser = BuilderParserXml.new(@xmlfile)
    parser.configure
    builder_gen = BuilderGenerator.new(parser.configuration)
    builder_gen.configure(@reader)
  end

end#ReaderBuilderTextTwoPass


# TWO PASS BUILDER FOR TEXT DSL AND GENERATED EXTERNAL FILE

class ReaderBuilderTextExternal

  # takes text DSL file name and filename for generated code
  def initialize(dslfile, genfile)
    @dslfile = dslfile
    @genfile = genfile
  end

  def configure(reader)
    parser = BuilderParserText.new(@dslfile)
    parser.configure
    builder_gen = BuilderGeneratorExternal.new(parser.configuration,
                                               @genfile)
    builder_gen.configure(reader)
  end

end#ReaderBuilderTextExternal


# TWO-PASS BUILDER FOR XML DSL AND GENERATED EXTERNAL FILE

class ReaderBuilderXmlExternal

  # takes XML DSL file name and output file name for generated code
  def initialize(dslfile, genfile)
    @dslfile = dslfile
    @genfile = genfile
  end

  def configure(reader)
    parser = BuilderParserXml.new(@dslfile)
    parser.configure
    builder_gen = BuilderGeneratorExternal.new(parser.configuration,
                                               @genfile)
    builder_gen.configure(reader)
  end

end#ReaderBuilderXmlExternal


# CLASSES USED TO HOLD THE DATA READ

class ServiceCall
end#ServiceCall

class Usage
end#Usage


# TESTING THE TWO PASS BUILDER AND FRAMEWORK

class TestTwoPass

  def TestTwoPass.run

    puts "\nTesting text DSL parser and direct configuration."
    rdr = ReaderFramework::Reader.new
    builder = ReaderBuilderTextTwoPass.new("dslinput.txt")
    builder.configure(rdr)
    inp = File.new("fowlerdata.txt")
    res = rdr.process(inp)
    inp.close
    res.each {|o| puts o.inspect}

    puts "\nTesting XML DSL parser and direct configuration."
    rdr = ReaderFramework::Reader.new
    builder = ReaderBuilderXmlTwoPass.new("dslinput.xml")
    builder.configure(rdr)
    inp = File.new("fowlerdata.txt")
    res = rdr.process(inp)
    inp.close
    res.each {|o| puts o.inspect}

    puts "\nTesting text DSL parser and external builder."
    rdr = ReaderFramework::Reader.new
    builder = ReaderBuilderTextExternal.new("dslinput.txt",
                                     "GeneratedBuilderExternal.rb")
    builder.configure(rdr)
    inp = File.new("fowlerdata.txt")
    res = rdr.process(inp)
    inp.close
    res.each {|o| puts o.inspect}

    puts "\nTesting XML DSL parser and external builder."
    rdr = ReaderFramework::Reader.new
    builder = ReaderBuilderXmlExternal.new("dslinput.xml",
                                     "GeneratedBuilderExternal.rb")
    builder.configure(rdr)
    inp = File.new("fowlerdata.txt")
    res = rdr.process(inp)
    inp.close
    res.each {|o| puts o.inspect}  end

end#TestTwoPass
