EchoPoint now has Cascading Style Sheet support
By Brad Baker
Sunday, August 10, 2003
The EchoPoint Cascading Style Sheet support has gone through some major improvements starting with version 0.5.
The main change is that all EchoPoint and Echo components now have a StyleInfo class associated with them that provides all the Style related information.
This allows Style introspection on a component, much like how you can introspect a JavaBean. In fact the StyleIntrospector is modelled very closely to resemble the standard java.bean introspection.
Introducing the EchoPoint CSS support
EchoPoint offers the ability to "externalize" the look of Echo Components into style sheet files. These files look like classic Cascading Style Sheet (CSS) files.
But first some background. Echo has had "Style" support for some time. You could create a nextapp.echo.Style object, add visual attributes into it via name and then apply them to a Component. For example :
Style largeTextStyle = new Style();
largeTextStyle.setAttribute(Component.STYLE_FONT, new Font(Font.VERDANA,Font.PLAIN,28);
Label label1 = new Label();
label1.applyStyle(largeTextStyle);
Label label2 = new Label();
label2.applyStyle(largeTextStyle);
However this requires a fair amount of repetitive Java code. Style objects also must be "hard coded" into the application, and hence lacked flexibility.
EchoPoint has a StyleSheet interface and CssStyleSheet class that provides "cascading" Style processing as well as the ability to load these Style definitions from a file. This file format, deliberately, looks a lot like a W3C Cascading Style Sheet file (CSS).
Note : You cannot use a traditional HTML CSS file with Echo components, but they are very similar.
For example to set the background and font to all Components within a nextapp.echo.Window you would create a CSS file like :
Component {
background : #ffee99;
font : font(verdana,plain,28);
}
Then in the EchoInstance.init() method you would do something like :
public nextapp.echo.Window init() {
Window w = new Window();
w.setTitle("Styled Window");
try {
StyleSheet styleSheet = CssStyleSheet.getInstance("/home/look.css");
styleSheet.applyTo(w,true);
} catch (StyleSheetParseException spe) {
System.out.println(spe);
}
return w;
}
Note the method styleSheet.applyTo(w,true);
This instructs the StyleSheet to apply itself to the Component w (a Window in this case) and to all its lowel level children .
The true parameter causes the style sheet to listen for future children of w and apply the applicable styles to those children. You can stop this behavior by calling :
styleSheet.stopListeningTo(w);
You can just apply a StyleSheet to a component and all its children by calling :
styleSheet.applyTo(w);
This form does not listen for future child components.
The provided implementation of echopoint.stylesheet.StyleSheet is echopoint.stylesheet.CssStyleSheet.
Cascading Class Style Support
The CssStyleSheet class applies styles in least specific class to most specific class order.
This means it that you can write a style sheet like :
Component {
background : red;
font : font(verdana,plain,28);
}
TextField {
font : font(helvetica,bold,28);
}
The above style sheet will cause all Components to have a red background and also a plain 28 point Verdana font. However TextField derived components will have a red background and a bold 28 point Helvetica font.
If you derive your own components from TextField, they will also have these overridden Style settings.
This makes the application of style sheets very powerful and allows the developer to create style sheets by building up visual aspects and by getting more specific with their style sheet entries. And these attributes "cascade" down the Java class heirarchy.
The CssStyleSheet class is responsible for parsing the style sheet file and creating the nextapp.echo.Style objects that will be applied to components. Entries in the style sheet file can have either fully qualified class names or the short versions e.g..
Component {
background : blue;
}
or
nextapp.echo.Component {
background : blue;
}
You can specific more than one class name on a single entry, as in :
DatePicker, ColorChooser, TextArea {
foreground : blue;
font : arial,bold,8;
}
Note: The names of the class selectors ARE case sensitive. This is in contrast to the style attribute names and values which are NOT case sensitive. So in the above example DatePicker must be in that exact case, while foreground, ForeGround, foreGround and fOregrOund are all equally valid.
The entries can be grouped by component identifier or via a group name. For example the style sheet :
Component {
foreground: blue;
}
Component#idName {
foreground: red;
}
Component!groupName {
foreground: orange;
}
will apply a blue foreground to all Components except those components in the group "groupName" will have a orange foreground, while the components with a identifier of "idName" will have an red foreground.
The order of style application is always
-
via javaClass
-
via groupName
-
via id
Actually the CssStyleSheet code uses a more complex scheme to determine both the groupName and identifier. It uses the attributed identifier support from EchoPoint, which allows you to have multiple, named attribute values in the one Component.getIdentifier() object.
For groups it look for an identifier attribute called "styleGroup". For id is first look for an attribute called "style". If this is not present, then it will attempt to use the value if the identifier as a whole. For a fuller explanation of these attributed identifiers, have a look at this article.
In practice is is very easy to "name" components and the groups and id's that the CSS code should use. For example :
TextField tf = new TextField();
tf.setIdentifier("styleGroup:dataEntry; style:important;");
// or
IdKit.set(tf,"styleGroup","dateEntry");
IdKit.set(tf,"style","important");
Component StyleInfo StyleSheet Support
The CssStyleSheet class looks for a supporting StyleInfo class in and around a component class when it parses style sheets. This StyleInfo support class returns information about a particular Style attribute, including its case senstive name and the class of the expected attribute value, as well as the bean property name that can be used to get the current value of a style attribute..
This allows the CssStyleSheet class to be more robust when parsing a style sheet, and ensure invalid values are not set into Style objects.
For example imagine the following style sheet entry :
DatePicker {
foreground : blue;
font : color(255,0,255);
}
Now without an special StyleInfo support, the CssStyleSheet class might read this stylesheet and place a Color object into a style attribute called "font". When the Component.applyStyle() ran, it would try to cast the Color object into a Font object and the program would bomb with a ClassCastException.
With the StyleInfo support, the CssStyleSheet class is able to "know" for a given Component class, what style attribute names are vaild, what class a style value is expected to be. This allows the CssStyleSheet class to check CSS entries, and if they are not valid , throw an exception at parse time, not at later at Style application time.
The StyleInfo support is modelled closely on the standard JavaBean introspection support. In JavaBeans a bean implementor who wishes to provide explicit information about their bean may provide a BeanInfo class that implements the BeanInfo interface and provides explicit information about the methods, properties, events, etc, of their bean.
Likewise in EchoPoint CSS, the component implementor may provide a StyleInfo class that implements the StyleInfo interface and provides explicit information about style attribute names, attribute value classes, valid Symbolic values for a Style attribute and the how bean properties that related to the style attributes.
There is a StyleSheetIntrospector class that looks for StyleInfo support classes. Once it finds them, it caches the results so that the information can be found more quickly the next time. This also reduces the number of StyleInfo instances to be one per Component class encountered.
The StyleSheetIntrospector works like this. For a given component class MyComponent:
-
It looks first for a MyComponentStyleInfo class that implements StyleInfo in the same package as the MyComponent.
-
Failing that it looks for the first nested public static class inside the MyComponent class that implements StyleInfo
After this processing is completed, the StyleSheetIntrospector inspects all interfaces of the component class and looks for nested public static inner classes that implement StyleInfo.
The union of all the found StyleInfo classes is returned in a wrapper StyleInfo class and cached against that component class. The next time the StyleSheetIntrospector is asked for this information, it will return the cached StyleInfo object.
For example given :
public class MyComponent extends Component implements Slip, Slop, Slap {
...
public static NestStyleInfo implements StyleInfo {
...
...
}
...
}
The introspector will examine first for a MyComponentStyleInfo support clas. If this fails it will look for a nested StyleInfo class inside the MyComponent class. The it will examine the Slip, Slop and Slap interfaces to see if they have nested public StyleInfo support classes inside them. In this case it would be the class called NestedStyleInfo.
This StyleInfo is then combined into one logical StyleInfo class and cached against the Component class.
The StyleInfo interface is as follows :
/**
* The StyleInfo interface can be implemented by a Component "helper"
* class to inform the StyleSheet system about the style attributes
* it supports. The StyleInfo implementing class should be a lightweight
* class since it will be instantiated by the StyleSheetIntrospector dynamically
* to provide style information.
*/
public interface StyleInfo {
/**
* Returns an array of StyleAttrDescriptor objects for a given
* component class.
*
* @return - an array of StyleAttrDescriptor objects. May return null.
*/
StyleAttrDescriptor[] getStyleDescriptors();
}
The getStyleDescriptors() method returns an array of StyleAttrDescriptor objects, one for each valid style attribute for the component class.
The StyleAttrDescriptor class contains
-
the name of the style attribute
-
the class of its style attribute value
-
any valid Symbolic values that can be set
-
and the bean property in the Component class that correlates to the style attribute.
The following is the StyleInfo support class used for nextapp.echo.Panel.
/**
* <code>StyleInfo</code> support for <code>nextapp.echo.Panel</code>
*/
public static class PanelStyleInfo implements StyleInfo {
private static StyleAttrDescriptor[] styleDescriptors = {
new StyleAttrDescriptor(Panel.STYLE_HORIZONTAL_ALIGNMENT,Integer.class,"horizontalAlignment",CssStyleSheetHelper.ecLeftCenterRight),
new StyleAttrDescriptor(Panel.STYLE_INSETS,Insets.class,"insets"),
new StyleAttrDescriptor(Panel.STYLE_VERTICAL_ALIGNMENT,Integer.class,"verticalAlignment",CssStyleSheetHelper.ecTopCenterBottom),
};
public StyleAttrDescriptor[] getStyleDescriptors() {
return styleDescriptors;
}
}
Notice that this support class describes the applicable styles for nextapp.echo.Panel, the expected class of each value and the bean property that correlates to the style attribute.
Also notice that some entries have an extra parameter which, if you dig down, is an array of SymbolicValue objects. SymbolicValue is an interface that allows a symbolic name to be associated with an actual Java object value. The CssStyleSheetHelper.ecLeftCenterRight array from above is defined as :
/**
* Helper static SymbolicValue[] - EchoConstants LEFT, CENTER and RIGHT
*/
public static SymbolicValue[] ecLeftCenterRight = {
new SymbolicIntegerValue(EchoConstants.LEFT,"left"),
new SymbolicIntegerValue(EchoConstants.CENTER,"center"),
new SymbolicIntegerValue(EchoConstants.RIGHT,"right"),
};
This means that the string "left" is associated with the integer value EchoConstants.LEFT.
This array also happens to contain the valid values that are allowed for the nextapp.echo.Panel horizontalAlignment property. The SymbolicValue interface allows the StyleSheet parser to be able to interpret symbolic strings and hence you can write a style sheet entry like :
nextapp.echo.Panel {
horizontalAlignment : left;
}
instead of
nextapp.echo.Panel {
horizontalAlignment : 1;
}
// Note : this is valid however using symbolic constants makes it more readable and debuggable
means that the string "left" is associated with the integer value EchoConstants.LEFT. This also happens to be the valid values that are allowed for the nextapp.echo.Panel horizontalAlignment property. The SymbolicValue interface allows the StyleSheet parser to be able to interpret symbolic strings and
Why Provide StyleInfo Support
Adding StyleInfo support to you Component class reduces the possibility of getting a Runtime ClassCastException. Also the use of this StyleInfo support allows the CssStyleSheet class to convert case insensitive CSS attribute names to case sensitive Style attribute names,
ie 'selectedfont' becomes 'selectedFont'.
If the Component class does NOT have the StyleInfo support, then the CssStyleSheet class first ask any CssStyleSheetHandlers if they know about the attribute, then if nothing is known about the style attribute, the CssStyleSheet class makes an intelligent guess about what type of attribute value object is required.
However all of support this is not fool proof, so the Component.applyStyle() method is placed inside an exception handler.
Any exception encountered running the Component.applyStyle() method is wrapped in a StyleSheetInvalidValueException, which contains the name of the offending CSS entry as well as the line number upon which it was encountered. This improves debugging of CSS files significantly.
Note you do not have to provide a StyleInfo support class. The CssStyleSheet will "attempt" to guess what you mean for a given style attribute, but using the StyleInfo support allows for more bullet proof style application.
Java Reflection Is Also Used
As a final step the CssStyleSheet will use reflection to apply style attributes that may not be applied during the Component.applyStyle() method. The SmartStyle class is used to track which style attributes have NOT been accessed during applyStyle().
It then uses reflection to inspect a Component and see if it has a "setter" method for the given style attribute name and value. If one can be found, then this is invoked and the style attribute will have been set. If one cannot be found, then the style attribute entry is ignored.
In practice this means that you can set ANY Component property that has an appropriate setter method and a attribute value that is understood by CssStyleSheet, such as Strings, Numbers, Fonts, Colors, ImageReferences etc....
The full list of well understood property values in defined in the class CssStyleSheetHelper.
Style Attribute Names
The names of the attributes, for a given class, are taken from its STYLE_xxxx constants. For example from nextapp.echo.Component we have
public static final String STYLE_BACKGROUND = "background";
public static final String STYLE_FONT = "font";
public static final String STYLE_FOREGROUND = "foreground";
therefore there are 3 attribute names supported : background, font and foreground.
Note : as we said before, style attribute names and values are NOT case sensitive, assuming there is StyleInfo meta data.
Style Attribute Values
The style attribute values can be numbers or strings, including quoted strings. For example
GroupBox {
borderSize : 1;
title : "My Title";
}
The CssStyleSheet class also handles a number of constructs for object style attribute values. For example nextapp.echo.Color objects can be specified as :
Component {
foreground : blue;
foreground : #0000FF;
foreground : color(0,0,255);
foreground : rgb(0,0,255);
}
Similarly
nextapp.echo.Font objects can be specified as :
Component {
font : font(verdana,plain,10);
font : verdana,plain,10;
}
And many symbolic constants are supportted for a given attribute. For example to set the TextComponent borderStyle you can write :
TextComponent {
borderStyle : solid;
}
The full list of attribute value forms in Backus Naur Form (BNF) follows :
attrValue ::= <string> |
<number> |
"null" |
<color> |
<font> |
<insets> |
<image> |
<corners> |
<rect> |
<intarray>
color ::= "#" <hexnumber> |
"color(" <number> "," <number> "," <number> ")" |
"rgb(" <number> "," <number> "," <number> ")" |
"black" | "blue" | "cyan" | "darkgray" | "green" |
"lightgray" | "magenta" | "orange" | "pink" | "red" |
"white" | "yellow"
insets ::= "insets(" <number> "," <number> "," <number> "," <number> ")" |
"insets(" <number> "," <number> ")" |
"insets(" <number> ")"
image ::= "image(" <string> "," <number> "," <number> ")" |
"image(" <string> ")"
corners ::= "corners(" <string> "," <number> "," <number> "," <string> "," <number> "," <number> ")" |
"corners(" <string> "," <number> "," <number> ")"
rect ::= "rect(" <number> "," <number> "," <number> "," <number> ")"
intarray ::= "int(" <number> [ "," <number> ] ")"
font ::= <fontName> "," <fontStyle> "," <fontSize> |
"font(" <fontName> "," <fontStyle> "," <fontSize> ")"
fontName ::= "arial" | "helvetica" | "monospace" | "sans_serif" | "sans serif" |
"serif" | "times" | "times_new_roman" | "times new roman" | "times_roman" |
"times roman" | "verdana"
fontStyle ::= "plain" | "bold" | "italic" | "underline" [ "|" <fontStyle> ]
fontSize ::= <number>
If you are wondering what BNF is and how to read it, look at : http://cui.unige.ch/db-research/Enseignement/analyseinfo/AboutBNF.html
Extending Style Sheet Processing
The CssStyleSheetHandler interface offers the ability to create your own handlers for setting style attributes. You would only do this if you have created your own Components with special style attributes and/or values, and you need to either parse these new attributes.
CssStyleSheet styleSheet = CssStyleSheet.getInstance();
styleSheet.addHandler(new MyStyleSheetHandler());
styleSheet.applyTo(component);
Since StyleSheet is an interface it is possible to write your own style sheet processor. For example you might want to support an XML based file format or you might want to have styles applied in a different order.
Why the CSS format was chosen
The reasoning behind the CSS file format was three fold. First the format is well documented and easily understood. Anyone with some familarity with W3C cascading style sheets should be able to easily created Echo ones.
The second reason was the "cascading" nature of W3C CSS support was required for Echo. Being able to override visual aspects based on Java class hierarchy is extremely powerful.
Finally an XML based syntax was investigated however the support classes (XML parser/ Xpath support /. DOM support etc....) meant that EchoPoint would triple in binary size and also the syntax of the style sheet file is less readable in XML.
The EchoPoint CSS text parsing code came to about 160 lines of Java code using the java..io.StreamTokenizer class, which equates to approximately 6K of disk space. The source code download for the Apache Xerces XML parser is currently 1600K, which makes it 266 times larger.
CSS BNF Syntax
If you are really keen on the CSS format, the full BNF syntax is as follows :
/**
*
* BNF (Backus Naur Form) of the EchoPoint CSS parser.
*
*/
styleEntry ::= <classNameSeq> "{"
[ <styleAttributeSeq> ]
"}"
classNameSeq ::= <classNameSelector> { "," <classNameSelector> }
styleAttributeSeq ::= attrName ":" attrValue ";"
classNameSelector ::= <javaClassName> |
<javaClassName>#<id> |
<javaClassName>!<group>
javaClassName ::= {<javaPackageName> "." } <identifier>
javaPackageName ::= <identifier>
id ::= <identifier>
group ::= <identifier>
attrName ::= <identifier>
attrValue ::= <string> |
<number> |
"null" |
<color> |
<font> |
<insets> |
<image> |
<corners> |
<rect> |
<intarray>
color ::= "#" <hexnumber> |
"color(" <number> "," <number> "," <number> ")" |
"rgb(" <number> "," <number> "," <number> ")" |
"black" | "blue" | "cyan" | "darkgray" | "green" |
"lightgray" | "magenta" | "orange" | "pink" | "red" |
"white" | "yellow"
insets ::= "insets(" <number> "," <number> "," <number> "," <number> ")" |
"insets(" <number> "," <number> ")" |
"insets(" <number> ")"
image ::= "image(" <string> "," <number> "," <number> ")" |
"image(" <string> ")"
corners ::= "corners(" <string> "," <number> "," <number> "," <string> "," <number> "," <number> ")" |
"corners(" <string> "," <number> "," <number> ")"
rect ::= "rect(" <number> "," <number> "," <number> "," <number> ")"
intarray ::= "int(" <number> [ "," <number> ] ")"
font ::= <fontName> "," <fontStyle> "," <fontSize> |
"font(" <fontName> "," <fontStyle> "," <fontSize> ")"
fontName ::= "arial" | "helvetica" | "monospace" | "sans_serif" | "sans serif" |
"serif" | "times" | "times_new_roman" | "times new roman" | "times_roman" |
"times roman" | "verdana"
fontStyle ::= "plain" | "bold" | "italic" | "underline" [ "|" <fontStyle> ]
fontSize ::= <number>
identiifer ::= <letter> { <letter> | <digit> | "_" }
string ::= any <letter> | """ { any <letter> } """ | "'" { any <letter> } "'"
number ::= <digit> { <digit> }
hexnumber ::= <hexdigit> { <hexdigit> }
hexdigit ::= <digit> | "A" | "B" | "C" | "D" | "E" | "F"
digit ::= "0" | "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9"