The from
header, for example, is normally styled using these CSS rules:
from { display: block; margin-left: 15ex; } from:before { display: marker; color: #004080; font-weight: bold; content: "From:"; }
To solve our problem, we need to replace:
content: "From:";
by:
content: invoke("localize", "from") ":";
where localize
is a custom method, which, given a key ("from
" in the above example), returns a localized string.
Localize
is a method defined in a class named StyleSheetExtension
(see samples/email/StyleSheetExtension.java
). The following at-rule, added at the top of email.css
, registers this class with XXE style engine —any class derived from abstract class ViewFactoryBase
— as being a stylesheet extension.
@extension "StyleSheetExtension navy white";
More precisely:
When a CSS stylesheet is loaded, the style engine searches @extension "
in it.arg0
arg1
... argN
"
If such at-rule is found, the style engine creates an instance of the class with fully qualified name arg0
and keeps this instance as long as the stylesheet is in use.
This class must have a public constructor with the following signature:
class_name
(String[]args
, ViewFactoryBaseviewFactory
)
When a property value contains a pseudo-function call like invoke(
, the style engine attempts to find a public method named arg0
, arg1
, ..., argN
)arg0
in the class described above.
This method must have the following signature:
StyleValuemethod_name
(StyleValue[]args
, NodecontextNode
, ViewFactoryBaseviewFactory
)
If such method is found, it is invoked and the returned result is used to specify the property value.
The constructor of StyleSheetExtension
is (excerpts from StyleSheetExtension.java
):
public StyleSheetExtension(String[] args, ViewFactoryBase viewFactory) { viewFactory.addDependency(EMAIL_NAMESPACE, "message", Namespace.XML, "lang"); . . . }
The | |
| |
When a CSS stylesheet contains a rule such as: p[align=left] { text-align: left; } the style engine knows that the view of a In |
The localize
method is implemented as follows (excerpts from StyleSheetExtension.java
):
public StyleValue localize(StyleValue[] args, Node contextNode, ViewFactoryBase viewFactory) { String text; if (args.length != 1 || (text = args[0].stringValue()) == null) { System.err.println("usage: content: invoke('localize', text);"); return null; } Element root = contextNode.getDocument().getRootElement(); String lang = root.getTokenAttribute(Name.XML_LANG, "en"); ResourceBundle messages = getMessages(lang); if (messages == null && !"en".equals(lang)) { messages = getMessages("en"); } String localizedText = null; if (messages != null) { try { localizedText = messages.getString(text); } catch (Exception ignored) {} } if (localizedText == null) { return args[0]; } else { return StyleValue.createString(localizedText); } } private static HashMap<String,ResourceBundle> langToMessages = new HashMap<String,ResourceBundle>(5); private static ResourceBundle getMessages(String lang) { ResourceBundle messages = langToMessages.get(lang); if (messages == null) { try { messages = ResourceBundle.getBundle("messages/Messages", new Locale(lang)); } catch (MissingResourceException ignored) {} if (messages != null) { langToMessages.put(lang, messages); } } return messages; }
The implementation of the localize
method is straightforward:
Get the xml:lang
attribute of the root
message element, if any. The value of this attribute specifies the language to use.
Try to load a ResourceBundle
containing messages localized to this language.
Use the key passed as an argument to read the localized text from the ResourceBundle
.
About the implementation:
| |
| |
public Type type; public Keyword keyword; public double number; public String string; public Name name; public StyleValue[] list; public Color color; public StringExpr xpath; The |
Stylesheet email.css
could have contained:
emphasis { display: inline; font-style: italic; } emphasis[role=bold] { font-style: normal; font-weight: bold; } emphasis[role=hightlight] { font-style: normal; background-color: navy; color: white; }
But email.css
does not contain any rule to style emphasis
elements. Instead, class StyleSheetExtension
extends class StyleSpecsBase
(which is an adapter class for interface StyleSpecs
) and its constructor registers the StyleSheetExtension
instance with the ViewFactoryBase
as being a set of intrinsic style specifications.
Excerpt from the StyleSheetExtension
constructor:
switch (args.length) { case 2: highlightForeground = StyleValue.parseColor(args[1]); /*FALLTHROUGH*/ case 1: highlightBackground = StyleValue.parseColor(args[0]); break; } if (highlightBackground == null) { highlightBackground = Color.yellow; } if (highlightForeground == null) { highlightForeground = Color.red; } viewFactory.addIntrinsicStyleSpecs( new EmphasisStyleSpecs(highlightBackground, highlightForeground)); viewFactory.addDependency(EMAIL_NAMESPACE, "emphasis", Namespace.NONE, "role");
The constructor is passed two strings " | |
The newly constructed | |
In |
Interface StyleSpecs
specifies the services expected by XXE style engine from a stylesheet.
An actual StyleSheet
of course implements StyleSpecs
. Custom code could also implement this interface to style a few, otherwise hard to style, elements. For example, such custom code is used to style HTML tables[5] and another custom code is used to style DocBook (CALS) tables.
If an implementation of StyleSpecs
has been registered, the style engine first uses it to find styles for the target of current CSS rule, then it uses the regular StyleSheet
to find more styles. Therefore the styles returned by the StyleSheet
may override those returned by the StyleSpecs
.
An implementation of StyleSpecs
generally just defines the following method:
int findStyleSpec(Element element, StyleSpec[] specs);
The style engine passes to the StyleSpecs
the element which is the target of current CSS rule and an array of 3 pre-created StyleSpec
data structures.
specs[StyleSpecs.ELEMENT]
must be filled with the styles of the element itself.
specs[StyleSpecs.BEFORE_ELEMENT]
must be filled with the styles of the content generated before the element.
specs[StyleSpecs.AFTER_ELEMENT]
must be filled with the styles of the content generated after the element.
The returned value is a mask specifying which one, of the 3 StyleSpec
data structures, has been filled with styles.
Excerpts from EmphasisStyleSpecs.java
:
private static final int ITALIC = 0; private static final int BOLD = 1; private static final int HIGHLIGHT = 2; private StyleValue fontStyleValue = StyleValue.createIdentifier(StyleValue.Keyword.NORMAL); private StyleValue fontWeightValue = StyleValue.createIdentifier(StyleValue.Keyword.NORMAL); private StyleValue backgroundColorValue = StyleValue.createColor(Color.white); private StyleValue colorValue = StyleValue.createColor(Color.black); ... @Override public int findStyleSpec(Element element, StyleSpec[] specs) { if (element.getName() == EMPHASIS) { styleEmphasis(element, specs[StyleSpecsBase.ELEMENT]); return StyleSpecsBase.ELEMENT_MASK; } return 0x0; } private void styleEmphasis(Element element, StyleSpec styleSpec) { int role = getRole(element); int nesting = 0; Element ancestor = element.getParentElement(); while (ancestor != null) { if (ancestor.getName() != EMPHASIS || getRole(ancestor) != role) { break; } ++nesting; ancestor = ancestor.getParentElement(); } switch (role) { case BOLD: if ((nesting % 2) == 0) { setFontWeight(styleSpec, StyleValue.Keyword.BOLD); } else { setFontWeight(styleSpec, StyleValue.Keyword.NORMAL); } break; case HIGHLIGHT: if ((nesting % 2) == 0) { setBackgroundColor(styleSpec, highlightBackground); setColor(styleSpec, highlightForeground); } else { setBackgroundColor(styleSpec, Color.white); setColor(styleSpec, Color.black); } break; default: if ((nesting % 2) == 0) { setFontStyle(styleSpec, StyleValue.Keyword.ITALIC); } else { setFontStyle(styleSpec, StyleValue.Keyword.NORMAL); } } } private static int getRole(Element element) { String role = element.getTokenAttribute(ROLE, null); if ("bold".equals(role)) { return BOLD; } else if ("highlight".equals(role)) { return HIGHLIGHT; } else { return ITALIC; } } private void setFontStyle(StyleSpec styleSpec, StyleValue.Keyword fontStyle) { fontStyleValue.initIdentifier(fontStyle); styleSpec.fontStyle = fontStyleValue; } private void setFontWeight(StyleSpec styleSpec, StyleValue.Keyword fontWeight) { fontWeightValue.initIdentifier(fontWeight); styleSpec.fontWeight = fontWeightValue; } private void setBackgroundColor(StyleSpec styleSpec, Color color) { backgroundColorValue.color = color; styleSpec.backgroundColor = backgroundColorValue; } private void setColor(StyleSpec styleSpec, Color color) { colorValue.color = color; styleSpec.color = colorValue; }
| |
The logic of | |
If the number of | |
The way XXE style engine is written allows to reuse pre-created | |
A public StyleValue marginTop = null; public StyleValue marginRight = null; public StyleValue marginBottom = null; public StyleValue marginLeft = null; public StyleValue paddingTop = null; public StyleValue paddingRight = null; public StyleValue paddingBottom = null; public StyleValue paddingLeft = null; public StyleValue borderStyle = null; public StyleValue borderWidth = null; public StyleValue borderTopColor = null; . . . |
A listitem
contained in an orderedlist
could be styled using the following rules:
listitem { display: block; } orderedlist > listitem { margin-left: 6ex; } orderedlist > listitem:before { display: marker; content: counter(n, decimal) "."; font-weight: bold; color: #004080; }
With the above rules, orderedlist
s with continuation
=continues
are not properly styled. Therefore, in email.css, last rule has been replaced by:
orderedlist > listitem:before { display: marker; content: invoke("listItemCounter"); font-weight: bold; color: #004080; }
Custom method listItemCounter
is implemented as follows (excerpts from StyleSheetExtension.java
):
public StyleValue listItemCounter(StyleValue[] args, Node contextNode, StyledViewFactory viewFactory) { int index = indexOfListItem((Element) contextNode); return StyleValue.createString(Integer.toString(1 + index) + '.'); } private static int indexOfListItem(Element listItem) { Element orderedList = listItem.getParentElement(); if (orderedList == null || orderedList.getName() != ORDEREDLIST) { return -1; } int index = orderedList.indexOfChildElement(listItem); int offset = 0; String continuation = orderedList.getNmtokenAttribute(CONTINUATION, "restarts"); if ("continues".equals(continuation)) { Element prevOrderedList = null; if (orderedList.getParentElement() != null) { Node node = orderedList.getPreviousSibling(); while (node != null) { if ((node instanceof Element) && ((Element) node).getName() == ORDEREDLIST) { prevOrderedList = (Element) node; break; } node = node.getPreviousSibling(); } } // Otherwise, orderedList is the root element. if (prevOrderedList != null) { Element last = prevOrderedList.getLastChildElement(); if (last != null) { offset = indexOfListItem(last) + 1; } // Otherwise, prevOrderedList is invalid. } } return (offset + index); }
In the solution of problem #1, we have already explained all the concepts behind a custom method such as StyleSheetExtension
.listItemCounter
. So what is new in problem #3?
With the above code, listitem
s contained in orderedlist
s with continuation
=continues
are properly styled. But if you insert or delete orderedlist
s in an email message, other orderedlist
s with continuation
=continues
are not properly updated.
Just invoking (excerpts from samples/email/StyleSheetExtension.java
)
viewFactory.addDependency(EMAIL_NAMESPACE, "orderedlist", Namespace.NONE, "continuation");
in the constructor of StyleSheetExtension
to declare a dependency between orderedlist
and its continuation
attribute is obviously not sufficient.
Here the idea is to write some custom code which observes modifications made to the email message and which rebuilds the views of orderedlist
s with continuation
=continues
when needed to.
This custom code is an implementation of interface BasicElementObserver
. A BasicElementObserver
must implement:
void elementChanged(DocumentEvent[] events);
This method is invoked by the CustomViewManager
of the ViewFactoryBase
each time the structure or the attributes (but not the text contained in mixed elements) of elements of interest have been modified. (More about CustomViewManager
s in next section.)
The implementation of BasicElementObserver
is created and registered with XXE style engine in the constructor of StyleSheetExtension
(excerpts from samples/email/StyleSheetExtension.java
):
CustomViewManager.NamePattern[] observed = { new CustomViewManager.NamePattern(EMAIL_NAMESPACE, "body"), new CustomViewManager.NamePattern(EMAIL_NAMESPACE, "listitem"), new CustomViewManager.NamePattern(EMAIL_NAMESPACE, "orderedlist") }; viewFactory.getCustomViewManager().add( new OrderedListObserver(viewFactory), observed);
A | |
This statement means: invoke |
Class OrderedListObserver
is implemented as follows (excerpts from OrderedListObserver.java
):
public class OrderedListObserver mplements CustomViewManager.BasicElementObserver { private DocumentView docView; private ArrayList<Element> orderedLists; ... public OrderedListObserver(StyledViewFactory viewFactory) { docView = viewFactory.getDocumentView(); orderedLists = new ArrayList<Element>(); } public void customViewAdded() {} public void customViewRemoved() {} public void elementChanged(DocumentEvent[] events) { orderedLists.clear(); for (int i = 0; i < events.length; ++i) { DocumentEvent event = events[i]; switch (event.getType()) { case CHILD_ADDED: case CHILD_REPLACED: case CHILD_REMOVED: { TreeEvent e = (TreeEvent) event; Element element = e.getElementSource(); if (element == null) { break; } if (element.getName() == ORDEREDLIST) { add(orderedLists, element); } else { if (isOrderedList(e.getOldChild()) || isOrderedList(e.getNewChild())) { // Add first child orderedlist (if any). Node node = element.getFirstChild(); while (node != null) { if ((node instanceof Element) && ((Element) node).getName() == ORDEREDLIST) { add(orderedLists, (Element) node); break; } node = node.getNextSibling(); } } } } break; // ------------------------------------------------------ // Left as an exercise, treat INCLUSION_UPDATED similarly // to CHILD_REPLACED // ------------------------------------------------------ case ATTRIBUTE_ADDED: case ATTRIBUTE_CHANGED: case ATTRIBUTE_REMOVED: { Element element = ((AttributeEvent) event).getElementSource(); if (element.getName() == ORDEREDLIST) { add(orderedLists, element); } } break; } } int count = orderedLists.size(); if (count > 0) { for (int i = 0; i < count; ++i) { Element orderedList = (Element) orderedLists.get(i); if (orderedList.getParentElement() != null) { // Add all following orderedlist siblings (if any). Node node = orderedList.getNextSibling(); while (node != null) { if ((node instanceof Element) && ((Element)node).getName() == ORDEREDLIST) { add(orderedLists, (Element) node); } node = node.getNextSibling(); } } // Otherwise, orderedList is the root element. } count = orderedLists.size(); for (int i = 0; i < count; ++i) { docView.rebuildView((Element) orderedLists.get(i)); } orderedLists.clear(); } } private static final void add(ArrayList<Element> orderedLists, Element orderedList) { if (!orderedLists.contains(orderedList)) { orderedLists.add(orderedList); } } private static final boolean isOrderedList(Node node) { if (node == null || !(node instanceof Element)) { return false; } else { return (((Element) node).getName() == ORDEREDLIST); } } }
The above implementation is simple but not very efficient:
First pass: add to set Document modifications are reported as | |
Add to the set the | |
Add to the set all the | |
Add to the set the | |
Second pass: For each | |
Third pass: rebuild the views of all the |
A smiley
element is styled as follows:
smiley { content: component("Smiley"); font: normal normal small sans-serif; /* Needed to display the red border of the selection */ display: inline-block; padding: 1px; }
Here, normal content has been replaced by custom content: component("Smiley")
. When XXE style engine finds the component
pseudo-function in a stylesheet, it creates an instance of the class whose fully qualified name has been passed as the argument of component
.
This class must implement interface ComponentFactory
.
Component createComponent(Element element,
Style style, StyleValue[] parameters,
StyledViewFactory viewFactory,
boolean[] stretch)
The factory is used to create a custom view of Element
element
.
This custom view may use the Style
of this element. (Our Smiley
example will use the font specified in the CSS rule: sans-serif, small.)
Passing extra parameters to component
after the fully qualified name of the factory class is possible. Theses parameters, which are StyleValue
s, that is parsed CSS property values, are passed in the parameters
array.
The factory can set stretch[0]
to true
if it wants the custom view to be enlarged or shrunken when the document view is itself enlarged or shrunken. stretch[0]
specifies this option for the width of the custom view. stretch[1]
, which specifies this option for the height of the custom view, is currently ignored.
The returned custom view is simply a newly created AWT component [7]which has been properly configured to render graphically its model: part or all of Element
element
.
Before really solving problem #4, we will explain here how to write the simplest custom views.
Alternate stylesheet email_passive_smiley.css
contains:
@import url(email.css); smiley { content: component("PassiveSmiley"); }
Class PassiveSmiley
is a very simplified version of Smiley
:
PassiveSmiley | Smiley |
---|---|
Represents a smiley element as a (borderless) JButton . | Represents a smiley element as a JComboBox . |
The value of the emotion attribute must be changed using the Attribute tool. | The JComboBox can be used to change the value of the emotion attribute, directly from the document view. |
Excerpts from PassiveSmiley.java
:
public class PassiveSmiley implements ComponentFactory { private static final Name EMOTION = Name.get("emotion"); public Component createComponent(Element element, Style style, StyleValue[] parameters, StyledViewFactory viewFactory, boolean[] stretch) { SmileyLabel smileyLabel = new SmileyLabel(element); smileyLabel.setFont(style.font); ComponentUtil.addFocusGainedListener(smileyLabel, element, viewFactory); return smileyLabel; } private static class SmileyLabel extends JButton implements TextLines { private Element element; public SmileyLabel(Element element) { // A borderless JButton looks like a JLabel but is focusable. setBorderPainted(false); setContentAreaFilled(false); String emotion = element.getNmtokenAttribute(EMOTION, "happy"); SmileyInfo smiley = null; SmileyInfo[] smileys = SmileyInfo.getKnownSmileys(); for (int i = 0; i < smileys.length; ++i) { if (smileys[i].getEmotion().equals(emotion)) { smiley = smileys[i]; break; } } if (smiley == null) { // Invalid emotion. Show it anyway. smiley = new SmileyInfo(emotion, null, "???"); } if (smiley.getIcon() != null) { setIcon(smiley.getIcon()); } setText(smiley.toString()); } public int getFirstBaseLine() { return ComponentUtil.getBaseLine(this); } public int getLastBaseLine() { return getFirstBaseLine(); } } }
The implementation of the | |
Implementing interface | |
Static method public class SmileyInfo { private String emotion; private Icon icon; private String asciiArt; public SmileyInfo(String emotion, Icon icon, String asciiArt) { this.emotion = emotion; this.icon = icon; this.asciiArt = asciiArt; } . . . public static SmileyInfo[] getKnownSmileys() { . . . } } Known smileys are listed in the |
PassiveSmiley
would not work very well without explicitly declaring a dependency between the smiley
element and its emotion
attribute in the constructor of StyleSheetExtension
:
viewFactory.addDependency(EMAIL_NAMESPACE, "smiley", Namespace.NONE, "emotion");
Like class PassiveSmiley
, class Smiley
also implements interface ComponentFactory
. Excerpts from Smiley.java
:
public class Smiley implements ComponentFactory { private static final Name EMOTION = Name.get("emotion"); public Component createComponent(Element element, Style style, StyleValue[] parameters, StyledViewFactory viewFactory, boolean[] stretch) { SmileyComboBox smileyCombo = new SmileyComboBox(element); smileyCombo.setFont(style.font); viewFactory.getCustomViewManager().add(smileyCombo, element, EMOTION); ComponentUtil.addFocusGainedListener(smileyCombo, element, viewFactory); return smileyCombo; } . . . }
| |
Unlike the void attributeValueChanged(DocumentEvent[] events); The above add statement registers the It means: invoke the | |
|
A CustomViewManager
is a helper object owned by the StyledViewFactory
(a class derived from abstract class ViewFactoryBase
). This object is a registry for custom views created by applying certain CSS rules to elements. This registry is cleared each time the StyleSheet
is changed using setStyleSheet
.
A rule like the following creates a custom view.
smiley { content: component("Smiley"); font: normal normal small sans-serif; /* Needed to display the red border of the selection */ display: inline-block; padding: 1px; }
Immediately after the creation of an active custom view, the factory that created it registers it with the CustomViewManager
.
The CustomViewManager
, which is a DocumentEventListener
, mainly forwards DocumentEvent
s to its registered custom views.
When and which DocumentEvent
s are sent to a custom view depend on the interface implemented by the custom view. We have already studied BasicElementObserver
and AttributeValueEditor
, but there are many other kinds of custom views: SimpleCounter
, BasicElementEditor
, SimpleElementEditor
, ElementEditor
, ElementValueEditor
.
An alternative to CustomViewManager
would be to have custom views directly implement interface DocumentEventListener
. In such case, all custom views (possibly thousands) would receive all document events. Not only this would make custom views tedious to write (because they would have to filter themselves uninteresting events) but this would also be extremely inefficient.
CustomViewManager
also notifies its registered custom views:
when a custom view is actually registered, by invoking method customViewAdded
;
when a custom view is no longer in use (because its model has been removed from the document), by invoking method customViewRemoved
;
when a custom view needs to update its model, by invoking method commitChanges
.
Imagine a JTextField
used to implement an attribute editor. User has typed a value in the JTextField
and then has typed Ctrl+S to save its document. Before the document is saved, commitChanges
instructs the JTextField
that the new value needs to be assigned to the attribute.
Excerpts from Smiley.java
:
private static class SmileyComboBox extends JComboBox implements CustomViewManager.AttributeValueEditor, TextLines { private Element element; private boolean performingAction = false; private boolean selectingItem = false; public SmileyComboBox(Element element) { super(SmileyInfo.getKnownSmileys()); setEditable(false); setRenderer(new SmileyRenderer()); this.element = element; updateSelectedItem(); addActionListener(new ActionListener() { public void actionPerformed(ActionEvent event) { if (selectingItem) return; SmileyInfo smiley = (SmileyInfo) getSelectedItem(); if (smiley != null) { performingAction = true; putAttribute(SmileyComboBox.this.element, EMOTION, smiley.getEmotion()); performingAction = false; } } }); } public void customViewAdded() {} public void customViewRemoved() {} public void commitChanges() {} public void attributeValueChanged(DocumentEvent[] events) { if (performingAction) { return; } updateSelectedItem(); } private void updateSelectedItem() { String emotion = element.getNmtokenAttribute(EMOTION, "happy"); SmileyInfo smiley = null; SmileyInfo[] smileys = SmileyInfo.getKnownSmileys(); for (int i = 0; i < smileys.length; ++i) { if (smileys[i].getEmotion().equals(emotion)) { smiley = smileys[i]; break; } } if (smiley == null) { // Invalid emotion. Show it anyway. smiley = new SmileyInfo(emotion, null, "???"); } selectingItem = true; // setSelectedItem triggers actionPerformed. setSelectedItem(smiley); selectingItem = false; } public int getFirstBaseLine() { return ComponentUtil.getBaseLine(this); } public int getLastBaseLine() { return getFirstBaseLine(); } }
Interactively selecting an item using the | |
The private static final String putAttribute(Element element, Name name, String value) { if (element.isEditable()) { return element.putAttribute(name, value); } else { return null; } } | |
When the | |
The |
[5] Even if this code has been written by XMLmind staff, it is technically custom code. That is,
this code could have been written by third-party programmers using documented APIs;
this code is not contained in xxe.jar
;
instead this code is dynamically discovered by XXE at startup time.
[6] Like body
, listitem
s can contain orderedlist
s.
[7] Note that a Swing JComponent is also an AWT Component.