Jsource

What if I told you that the following is a bit of Java code?

require 'jsource'

jclass JsrcTest do
  import %w|List ArrayList Map HashMap|.collect { |it| "java.util.#{it}" }

  # have a public void say(String text) { ... }
  say(:void, 'String text') {%|
    println(text);
  |}

  # have a public static void main(String[] args) { ... }
  # This uses an alternate form, instead of having the whole
  # block be a string.
  main {
    c 'if (args.length < 1) {'
    c '  println(main(3,14));'
    c '} else {'
    c '  new JsrcTest().say(args[0]);'
    c '}'
  }

  # Manually make a method.
  meth(:main,String,'int over, long loaded','static') {%|
    // Just an overload.
    return "Cor flipping blimey!";
  |}
end

[download]

You’d probably say “you’re lying!”. And you’d be right too – that’s blatantly not Java code, is it? But it could be, given the right jsource to require up there in line one.

What am I on about now? Well, let’s cut to the chase:

# A Java class
class JavaClass
  attr_accessor :name, :access, :superclass, :interfaces, :members, :package, :imports
  
  def initialize(name, access = :public, superclass = nil, interfaces = nil, &block)
    @name, @access, @superclass, @interfaces =  name, access, superclass, interfaces
    @members, @package, @imports = [], nil, []
    instance_eval(&block) if block
  end

  # To Java source
  def to_s
    %Q[
     #{"package #{package}" if package}
     #{imports.collect { |it| "import #{it};" }.join("\n")}
     #{access} class #{name}#{" extends #{superclass}" if superclass}#{" implements #{interfaces.join(', ')}" if interfaces} {
       #{members.collect { |it| it.to_s }.join("\n")}
     }\n]
  end

  # Add a memmber
  def <<(member)
    self.members << member
  end

  #--
  # HELPERS FOR USE IN CLASS BLOCK
  #++
  # Set the package
  def package(name = nil)
    if name
      @package = name
    else
      @package
    end
  end
  
  # Add an import
  def import(names)
    @imports += names
  end
  
  # Create a method
  def meth(name, ret_type = nil, args = [], access = :public,&block)
    self << JavaMethod.new(name,ret_type,args, access,&block)
  end

  # create a standard static main method
  def main(&block)
    self << JavaMethod.new(:main,:void,'String[] args','public static',&block)  
  end
  
  # allow methods to be defined like via method_missing too.
  def method_missing(sym, *args, &block)
    ret_type, arg_types, access = args
    access ||= :public
    meth(sym,ret_type,arg_types,access,&block)    
  end

  #--
  # COMMANDLINE
  #++
  def print(strm = 'STDOUT')
    rstrm = eval(strm)
    if rstrm.kind_of? IO
      rstrm << self
    else
      rstrm && raise("'#{strm}' is a #{rstrm.class}, not a stream!") or raise("wtf is '#{strm}' supposed to mean?")
    end      
  end

  def dump(fn = ($0.gsub(/(.)\..*$/,'\1') + '.java'))
    File.open(fn,'w+') do |f|
      f << self.to_s
    end
    fn  # allows use in compile
  end  

  def compile(*opts)
    exec "javac #{dump} #{opts.join(' ')}"
  end      
end

# A Java method
class JavaMethod
  attr_accessor :name, :ret_type, :arg_types, :access
  
  def initialize(name,  ret_type = nil, arg_types = '', access = [:public], &block)
    @name, @ret_type, @arg_types, @access, @code = name, ret_type, arg_types, access, []
    c instance_eval(&block) if block
  end

  def each_line(&block)
    self.code.each &block
  end

  # Get this method's Java code. Here is where we could do all
  # the magic to replace Ruby idioms with Java idioms
  def code
    @code.collect { |it| it.gsub(/([^.])println/,'\1System.out.println') if it }
  end

  def code=(the_code)
    @code = the_code
  end

  def to_s
    %Q[#{access} #{ret_type || 'void'} #{name}(#{arg_types if arg_types}) {
      #{self.code.join("\n")}
    }]
  end

  def [](i)
    self.code[i]
  end

  def []=(i,v)
    self.code[i] = v
  end

  def <<(*s)
    self.code += s
    nil   # so block using it doesn't also return it
  end

  alias :c :<<
end

# Whenever a missing const is requested, send back the requested
# const symbol as the const itself. *very* hacky but for this
# kind of toy it's probably okay...
class Module
  def const_missing(sym)
    sym
  end
end

######
## Helper to create a class
def jclass(name, access = :public, superclass = nil, interfaces = nil, &block)
  jc = JavaClass.new(name,access,superclass,interfaces,&block)
  if (arg = ARGV[0])
    jc.send(arg, *ARGV[1,ARGV.length])
  else
    jc.print              
  end  
end

[download]

So it’s just a couple of classes and a helper (plus a bit of module hackery), but already it’s the beginnings of Java code that doesn’t take months to type. Obviously it’s more difficult to help with actual method code, but it could be done – I had the idea of supporting stuff like hash and array literals, auto-parens on method calls, and a whole lot of other stuff.

The thing that (I think) is interesting about this (apart from the beautiful meta-programming stuff that Ruby supports) is the fact that it not only describes the Java source, but writes it directly. With a bit of judicious use of send and the command-line, I can easily set up to have the code dumped out to stdout, a file, or even compiled directly. If you’ve gone to the trouble of downloading both sources, save ‘em somewhere, go there, and do:

./JsrcTest.rb

and you should get a screenful of really messy (but valid) Java code. Better still,

./JsrcTest.rb dump

will write a file (JsrcTest.java), and

./JsrcTest.rb compile

Will run javac automagically for you.

And that, as they say, is that. Something to play with for half an hour, anyway… It could easily emulate attr_accessor et. al (and generate bean getters and setters), and I imagine quite a few other standard patters. I wonder exactly how much like Ruby we could make our Java…