Cheri -- A Builder Framework
Cheri is a framework for creating builder applications (those that create hierarchical, tree-like, structures). It includes a number of builders based on the framework, as well as a builder-builder tool for easily creating simple builders. Cheri also comes with a demo application, Cheri::JRuby::Explorer, that is built using two of the supplied builders (Cheri::Swing and Cheri::Html).
This version (0.0.7) is an early beta release. Some features are still not fully developed (though we're getting close). So do expect some bugs, especially in Cheri::JRuby::Explorer (CJX), which is very much a work in progress. I note some known problems in the CJX section below.
Documentation will be forthcoming over the coming days, so watch the Cheri pages at RubyForge for updates:
http://cheri.rubyforge.org/
http://rubyforge.org/projects/cheri
Quick Start
Cheri builders are mixin modules; to use one, you include it in a class. The builder's functionality is available to instances of that class, and any subclasses (unless the including class is Object ? inclusion in Object / at the top level is supported, but discouraged; inheritance is disabled in that case).
require 'rubygems'
require 'cheri/swing'
...
include Cheri::Swing
All Cheri builders implement a cheri
method (the proxy method), which plays two roles, depending on how it's called. When called with a block, the cheri method enables Cheri builder syntax within its scope for all included builders. When called without a block, it returns a CheriProxy object that can act as a receiver for builder methods, for any included builder.
@frame = cheri.frame('Hello!') #=> JFrame (for Cheri::Swing)
cheri {
@frame = frame('Hello!')
}
The cheri
method is also used to set global Cheri options. Currently only one global option, alias
, is defined:
cheri[:alias=>[:cbox,:check_box,:tnode,:default_mutable_tree_node]]
cheri.cbox #=> JCheckBox (Cheri::Swing)
cheri.tnode #=> DefaultMutableTreeNode (Cheri::Swing)
Each built-in Cheri builder also supplies its own proxy method (in addition to the cheri
method): swing
for Cheri::Swing (and also awt
, since Cheri::Swing includes Cheri::AWT), html
for Cheri::Html, and xml
for Cheri::Xml. These methods play the same dual scoping/proxy roles as the cheri
method, but apply only to their respective builders. (Each also provides additional functionality; see the sections on individual builders for details.) The builder-specific proxy methods also serve to disambiguate overloaded builder method names:
swing.frame #=> javax.swing.JFrame
awt.frame #=> java.awt.Frame
html.frame #=> HTML frame (Cheri::Html::EmptyElem)
Cheri::Swing
To include:
require 'rubygems'
require 'cheri/swing'
...
include Cheri::Swing
Note that inclusion at the top level is not recommended.
Options:
swing[:auto]
swing[:auto=>true] #=> Enables auto mode (no swing/cheri block required)
Cheri::Swing (which includes Cheri::AWT) includes methods (Ruby-cased class names) for all javax.swing, javax.swing.border and java.awt classes, plus many in javax.swing.table, javax.swing.tree, java.awt.image and java.awt.geom. You can extend Cheri::Swing with other classes/packages (including 3rd party, or your own!) using the Cheri builder-builder's build_package
method.
Cheri::Swing (and any other builder based on Cheri::Java) also provides easy-to-use on_xxx methods to implement event listeners. Any event listener supported by a class (through an addXxxListener method) can be accessed from Cheri::Swing using an on_xxx method (where xxx is the Ruby-cased event-method name). Because it is so widely used in Swing, the ActionListener#actionPerformed event method is aliased as on_click:
@frame = swing.frame('Hello') {
size 500,500
flow_layout
on_window_closing {|event| @frame.dispose}
('Hit me') {
on_click { puts 'button clicked' }
}
}
The cherify
and cheri_yield
methods can be used to incorporate objects created outside the Cheri::Swing framework (cherify
), or to re-introduce objects created earlier within the framework (cheri_yield
):
class MyButton < javax.swing.JButton
...
end
...
a_button = MyButton.new
...
@frame = swing.frame('Hello') {
size 500,500
flow_layout
cherify(a_button) {
on_click { puts 'button clicked' }
}
}
@frame = swing.frame('Hello') {
menu_bar {
@file_menu = menu('File') {
menu_item('Exit') {on_click {@frame.dispose } }
}
}
}
# => add a new item later:
cheri_yield(@file_menu) {
menu_item('Open...') {
on_click { ... }
}
}
The Cheri builder-builder can be used to extend Cheri::Swing in a couple of ways. Individual classes can be included using the build
statement, while entire packages can be included using the build_package
statement. Note that you may need to supply connection logic if the incorporated classes use methods other than add
to connect child objects to parent objects; see file /lib/cheri/builder/swing/connecter.rb for many examples.
// Java:
package my.pkg;
public class MyParent extends javax.swing.JComponent {
...
public void addChild(MyChild child) {
...
}
}
...
public class MyChild {
...
}
# JRuby:
require 'cheri/swing'
...
include Cheri::Swing
...
# easy-to-reference names; could use include_package instead
MyParent = Java::my.pkg.MyParent
MyChild = Java::my.pkg.MyChild
# example specifying each class; 'custom' names may be specified
MyBuilder = Cheri::Builder.new_builder do
extend_builder Cheri::Swing
build MyParent,:pappy
build MyChild,:kiddo
type MyParent do
connect MyChild,:addChild
end
end
include MyBuilder
@frame = swing.frame('My test') {
...
panel {
pappy {
kiddo { ... }
}
}
}
# example specifying package; default naming
MyBuilder = Cheri::Builder.new_builder do
extend_builder Cheri::Swing
build_package 'my.package'
type MyParent do
connect MyChild,:addChild
end
end
include MyBuilder
@frame = swing.frame('My test') {
...
panel {
my_parent {
my_child { ... }
}
}
}
You can also use the builder-builder just to add conection logic to Cheri::Swing, as not every possible connection type is defined.
See the Cheri::JRuby::Explorer (CJX) code (under lib/cheri/jruby/explorer) for extensive examples of Cheri::Swing usage.
Cheri::Xml
To include:
require 'rubygems'
require 'cheri/xml'
...
include Cheri::Xml
Note that inclusion at the top level is not recommended.
Options:
xml[:any]
xml[:any=>true] #=> Any tag name inside xml {} will be accepted
xml[:accept=>[:aaa,:bbb,:nnn]] #=> only specified tag names accepted
(see builder-builder example below for alternative approach)
xml[:format]
xml[:format=>true] #=> output formatted with line-feeds only
xml[:indent] #=> output indented by 2 spaces per level
xml[:indent=>nnn] #=> output indented by nnn spaces per level
xml[:margin=>nnn] #=> output indented by margin (in addition to :indent)
xml[:esc]
xml[:esc=>true] #=> output will be escaped (off by default for performance)
xml[:ns=>:xxx] #=> declare xxx as a namespace prefix
xml[:ns=>[:xxx,:yyy,:zzz...]] #=> declare xxx,yyy,zzz as namespace prefixes
xml[:alias=>[:alias1,:name1,:alias2,:name2...] #=> declare tag aliases
xml[:attr=>[:alias1,:attr1...]] #=> declare attribute aliases
Options specified using xml apply to all threads for an instance. Options specified using xml(opts) apply only to the current thread/scope:
# example
xml[:any=>true,:indent=>3,:esc=>false]
@out = xml {
# nothing escaped at this level
aaa{
bbb {
xml(:esc=>true) {
# everything escaped in this scope
ddd { ... }
eee { ... }
}}}}
The result of an xml
block will be one of several types of object, depending on the tags used and how they are invoked. The result object can be coerced to a String, directly by calling its #to_s method, or indirectly by using << to append it to a String or IO stream. The #to_s method also takes an optional String/stream parameter; for streams, this is the most efficient way to render the XML.
# example
xml[:any,:indent]
@result = xml{
aaa(:an_attr='a value',:another=>'value 2') {
bbb { ccc }
}
}
puts @result #=> XML
@result.to_s #=> XML
a_string << @result #=> appends XML
a_stream << @result #=> appends XML
@result.to_s(a_string) #=> appends XML more efficiently
@result.to_s(a_stream) #=> appends XML more efficiently
# result:
<?xml version="1.0" encoding="UTF-8"?>
<aaa another="value 2" an_attr="a value">
<bbb>
<ccc />
</bbb>
</aaa>
To omit the XML declaration, use xml
as the receiver for the initial element:
xml.aaa{bbb}
# result
<aaa>
<bbb />
</aaa>
Alias element names that are lengthy, or can't be used directly in Ruby:
xml[:alias=>[:cls,:class]]
xml.aaa{cls}
# result
<aaa>
<class />
</aaa>
Declare namespace prefixes, and apply them directly (using myns.tag or myns::tag), or apply them to all elements in a scope:
xml[:alias=>[:env,:Envelope,:hdr,:Header,:body,:Body]]
xml[:ns=>:soap]
xml { soap {
env(:xxx=>'yyy') {
hdr
body
}}}
# result
<?xml version="1.0" encoding="UTF-8"?>
<soap:Envelope xxx="yyy">
<soap:Header />
<soap:Body />
</soap:Envelope>
Use no_ns to turn off a namespace, or specify a different namespace:
xml[:alias=>[:env,:Envelope,:hdr,:Header,:body,:Body]]
xml[:ns=>[:soap,:xx]]
xml {
aaa {
soap { env {
hdr
body {
no_ns {
bbb
xx::ccc
ddd
xx {eee; fff}
}}}}}}
# result
<?xml version="1.0" encoding="UTF-8"?>
<aaa>
<soap:Envelope>
<soap:Header />
<soap:Body>
<bbb />
<xx:ccc />
<ddd />
<xx:eee />
<xx:fff />
</soap:Body>
</soap:Envelope>
</aaa>
Use the Cheri builder-builder to define more explicit element relationships:
require 'cheri/xml'
my_content_elems = [:aaa,:bbb,:ccc]
my_empty_elems = [:xxx,:yyy]
MyBuilder = Cheri::Builder.new_builder do
extend_builder Cheri::Xml
build Cheri::Xml::Elem,my_content_elems
build Cheri::Xml::EmptyElem,my_empty_elems
symbol :aaa { connect :bbb,:ccc }
symbol :bbb { connect :xxx }
symbol :ccc { connect :yyy }
# raise error to prevent non-connects from silently failing
type Cheri::Xml::XmlElement do
connect Cheri::Xml::XmlElement do |parent,child|
raise TypeError,"can't add #{child.sym} to #{parent.sym}"
end
end
end
include Cheri::Xml
include MyBuilder
Cheri::Html
Documentation TBD
Options:
html[:format]
html[:format=>true] #=> output formatted with line-feeds only
html[:indent] #=> output indented by 2 spaces per level
html[:indent=>nnn] #=> output indented by nnn spaces per level
html[:margin=>nnn] #=> output indented by margin (in addition to :indent)
html[:esc]
html[:esc=>true] #=> output will be escaped (off by default for performance)
Cheri builder-builder
Documentation TBD
Cheri::JRuby::Explorer (CJX)
CJX is a Swing application written entirely in (J)Ruby using the Cheri::Swing and Cheri::Html builders. It enables you to easily browse classes/modules, configuration/environment settings, and, if ObjectSpace is enabled, any objects in a JRuby instance. A small DRb server component can be installed in other JRuby instances, enabling you to browse them as well. (Note that I have been trying to get the DRb server component working in C/MRI Ruby as well, but have run up against threading/IO conflicts. Suggestions welcome!)
The CJX client requires JRuby 1.0.0RC3 or later. To run it (after installing the Cheri gem):
require 'rubygems'
require 'cheri/jruby/explorer'
Cheri::JRuby::Explorer.run
Alternatively, you can load and run it in one step:
require 'rubygems'
require 'cheri/cjx'
This will take several seconds to load and start -- performance will be one area of ongoing improvement. Once it loads, it should be fairly clear what to do.
Some known issues:
-
Browsing the class hierarchy is very slow right now -- this actually slowed down in the past couple of days when I switched from HTML to straight Swing layout, the opposite of what I expected to happen.
-
There are lots of layout issues; neither HTML (JEditorPane) nor BoxLayout provide exactly what I'm looking for. Will probably have to bite the bullet and go to GridBagLayout. Ugh.
-
Global variables are currently shown, um, globally, when many of them should be shown per thread. This will be fixed in a later version, which will include a Thread section with other goodies as well (thread-local vars, status, etc.).
To install the CJX DRb server component in an instance (assuming the Cheri gem is installed):
require 'rubygems'
require 'cheri/explorer'
Cheri::Explorer.start nnnn #=> where nnnn is a port number
Note that for the server, you require 'cheri/explorer', not 'cheri/jruby/explorer'. Also note that the above actually does work in C/MRI Ruby, but requests to the server then hang in CJX, unless you join the thread:
Cheri::Explorer.thread.join
After that, you can browse just fine in CJX, but you can't do anything more in the C-Ruby instance, so it's kind of pointless. Again, if anyone with some DRb experience (of which I have none) can offer any suggestions, I'd appreciate it.
The Rest
Please visit the Cheri site for more documentation, I'll be continually adding to it in the coming days.
Bill Dortch (cheri dot project aaat gmail dot com) 19 June 2007