001: /* AnnotateDataBinder.java
002:
003: {{IS_NOTE
004: Purpose:
005:
006: Description:
007:
008: History:
009: Thu Nov 16 13:22:37 2006, Created by Henri Chen
010: }}IS_NOTE
011:
012: Copyright (C) 2006 Potix Corporation. All Rights Reserved.
013:
014: {{IS_RIGHT
015: }}IS_RIGHT
016: */
017: package org.zkoss.zkplus.databind;
018:
019: import org.zkoss.zk.ui.Path;
020: import org.zkoss.zk.ui.Page;
021: import org.zkoss.zk.ui.Desktop;
022: import org.zkoss.zk.ui.Component;
023: import org.zkoss.zk.ui.UiException;
024: import org.zkoss.zk.ui.sys.ComponentCtrl;
025: import org.zkoss.zk.ui.metainfo.Annotation;
026:
027: import java.util.Map;
028: import java.util.List;
029: import java.util.ArrayList;
030: import java.util.Map.Entry;
031: import java.util.Iterator;
032:
033: /**
034: * <p>The DataBinder that reads ZUML annotations to create binding info. The ZUML page must declare the
035: * XML namespace, xmlns:a="http://www.zkoss.org/2005/zk/annotation", to use the ZUML annotations.
036: * The annotation is declared before each Component or specified directly on the Component attributes.
037: * For example, the following annotation associates the
038: * attibute "value" of the component "textbox" to the bean's value "person.address.city".</p>
039: * <pre>
040: * <a:bind value="person.address.city"/>
041: * <textbox/>
042: * </pre>
043: * <p>Or since ZK 2.4 you can annotate directly on the attribute "value" of the component "textbox" like this.</p>
044: * <pre>
045: * <textbox value="@{person.address.city}"/>
046: * </pre>
047: * <p>The @{...} pattern tells the ZUML parser that this is for annotation.</p>
048: *
049: * <p>The AnnotateDataBinder knows "a:bind" annotation only. The complete format is like this if you declared it
050: * before the Component.
051: * <pre>
052: * <a:bind attrY="bean's value;[tag:expression]..."/>
053: * <componentX/>
054: * </pre>
055: *
056: * <p>You can also specify directly on the Component attribute, the complete format is like this:
057: * <pre>
058: * <componentX attrY="@{bean's value,[tag='expression']...}"/>
059: * </pre>
060: *
061: * <p>This associates the componentX's attribute attrY to the bean's value. The bean's value is something
062: * in the form of beanid.field1.field2... You can either call {@link DataBinder#bindBean} to bind the beanid to a
063: * real bean object or you can neglect it and this DataBinder would try to find it from the variables map via
064: * ({@link org.zkoss.zk.ui.Component#getVariable} method. That is, all those variables defined in zscript are
065: * accessible by this DataBinder. Note that you can choose either two formats of annotaion as your will and you
066: * can even hybrid them together though it is generaly not a good practice.</p>
067: *
068: * <p>The tag:expression or tag='expression' is a generic form to bind more metainfo to the attrY of the componentX.
069: * The currently supported tag includes "load-when", "save-when", "access", and "converter"</p>
070: *
071: * <ul>
072: * <li>load-when. You can specify the events concerned when to load the attribute of the component from the bean.
073: * Multiple definition is allowed and would be called one by one.
074: * For example, the following code snip tells DataBinder that the attribute "value" of Label "fullName" will load
075: * from "person.fullName" when the Textbox "firstName" or "lastName" fire "onChange" event.
076: *
077: * <p>Declare in front of the Component:</p>
078: * <pre>
079: * <a:bind value="person.firstName"/>
080: * <textbox id="firstname"/>
081: *
082: * <a:bind value="person.lastName"/>
083: * <textbox id="lastname"/>
084: *
085: * <a:bind value="person.fullName; load-when:firstname.onChange; load-when:lastname.onChange"/>
086: * <label id="fullname"/>
087: * </pre>
088: * <p>Or specify directly on the Component's attribute:</p>
089: * <pre>
090: * <textbox id="firstname" value="@{person.firstName}"/>
091: * <textbox id="lastname" value="@{person.lastName}"/>
092: * <label id="fullname" value="@{person.fullName, load-when='firstname.onChange,lastname.onChange'}"/>
093: * </pre>
094: * </li>
095: *
096: * <li>save-when. You can specify the events concerned when to save the attribute of the component into the bean.
097: * Since ZK version 3.0.0, you can specify multiple events in save-when tag (i.e. before ZK 3.0.0, you can specify only
098: * one event). The events specified, if fired, will trigger
099: * this DataBinder to save the attribute of the component into bean. For example, the following code snip tells
100: * DataBinder that the attribute "value" of Textbox "firstName" will
101: * save into "person.firstName" when the Textbox itself fire "onChange" event.
102: *
103: * <p>Declare in front of the Component:</p>
104: * <pre>
105: * <a:bind value="person.firstName; save-when:self.onChange"/>
106: * <textbox id="firstName"/>
107: * </pre>
108: *
109: * <p>Or specify directly on the Component's attribute:</p>
110: * <pre>
111: * <textbox id="firstName" value="@{person.firstName, save-when='self.onChange'}"/>
112: * </pre>
113: *
114: * <p>However, you don't generally specify the save-when tag. If you don't specify it, the default events are used
115: * depends on the natural charactieric of the component's attribute as defined in lang-addon.xml. For example,
116: * the save-when of Label.value is default to none while that of Textbox.value is default to self.onChange.
117: * That is, the following example is the same as the above one.</p>
118: * <p>Declare in front of the Component:</p>
119: * <pre>
120: * <a:bind value="person.firstName"/>
121: * <textbox id="firstName"/>
122: * </pre>
123: * <p>Or specifies directly on the Component's attribute:</p>
124: * <pre>
125: * <textbox id="firstName" value="@{person.firstName}"/>
126: * </pre>
127: *
128: * <p>On the other hand, you might not specify the save-when tag nor you want the default events to be used. Then you
129: * can specify a "none" keyword or simply leave empty to indicate such cases.</p>
130: * <pre>
131: * <a:bind value="person.firstName; save-when:none;"/>
132: * <textbox id="firstName"/>
133: * </pre>
134: * or
135: * <pre>
136: * <a:bind value="person.firstName; save-when: ;"/>
137: * <textbox id="firstName"/>
138: * </pre>
139: * or
140: * <pre>
141: * <textbox id="firstName" value="@{person.firstName, save-when='none'}"/>
142: * </pre>
143: * or
144: * <pre>
145: * <textbox id="firstName" value="@{person.firstName, save-when=''}"/>
146: * </pre>
147: * </li>
148: *
149: * <p>Since 3.0.0, DataBinder supports validation phase before saving attribute content into bean property when
150: * triggered by the specified event in save-when tag. It will fire onBindingSave event to the data-binding component and
151: * then fire onBindingValidate to the triggering component before really saving component attribute contents into bean's
152: * property. So application developers get the chance to handle the value validatiion before saving. In the following example
153: * when end user click the "savebtn" button, an "onBindingSave" is first fired to "firtName" and "lastName" textboxes and
154: * then an "onBindingValidate" is fired to "savebtn" button. Application developers can register proper event handlers to do
155: * what they want to do.</p>
156: * <pre>
157: * <textbox id="firstName" value="@{person.firstName, save-when="savebtn.onClick"}" onBindingSave="..."/>
158: * <textbox id="lastName" value="@{person.lastName, save-when="savebtn.onClick"}" onBindingSave="..."/>
159: * <button id="savebtn" label="save" onBindingValidate="..."/>
160: * </pre>
161: *
162: * <p>Note that the original textbox constraint mechanism is still there. This DataBinder validation phase is an
163: * add-on feature that can be applied to all components and attributes that use data binding mechanism.</p>
164: *
165: * <li>access. You can set the access mode of the attrY of the componentX to be "both"(load/save),
166: * "load"(load Only), "save"(save Only), or "none"(neither). Multiple definition is NOT allowed
167: * and the later defined would
168: * override the previous defined one. The access mode would affects the behavior of the DataBinder's loadXxx
169: * and saveXxx methods.
170: * The {@link DataBinder#loadAll} and {@link DataBinder#loadComponent} would load only those attributes
171: * with "both" or "load" access mode. The {@link DataBinder#saveAll} and
172: * {@link DataBinder#saveComponent} would save only those attributes with "both" or "save" access mode. If you
173: * don't specify it, the default access mode depends on the natural characteristic of the component's attribute
174: * as defined in lang-addon.xml. For example, Label.value is default to "load" access mode while Textbox.value
175: * is default to "both" access mode. For example, the following code snips tells DataBinder that Textbox "firstName"
176: * would allowing doing save into bean only not the other way.
177: *
178: * <p>Declare in front of the Component:</p>
179: * <pre>
180: * <a:bind value="person.firstName;access:save;"/>
181: * <textbox id="firstName"/>
182: * </pre>
183: *
184: * <p>Or specify directly on the Component's attribute:</p>
185: * <pre>
186: * <textbox id="firstName" value="@{person.firstName, access='save'}"/>
187: * </pre>
188: * </li>
189: *
190: * <li>converter. You can specify the class name of the converter that implments the {@link TypeConverter} interface.
191: * It is used to convert the value between component attribute and bean field. Multiple definition is NOT allowed
192: * and the later defined would override the previous defined one.
193: * Most of the time you don't have to specify this since this DataBinder supports converting most commonly
194: * used types. However, if you specify the TypeConverter class name, this DataBinder will new an instance and use
195: * it to cast the class.
196: * </li>
197: * </ul>
198: *
199: * @since 2.4.0 Supporting @{...} annotations.
200: * @since 3.0.0 Supporting multiple events of save-when tag and validation phase.
201: * @author Henri Chen
202: * @see AnnotateDataBinderInit
203: * @see DataBinder
204: */
205: public class AnnotateDataBinder extends DataBinder {
206: /**
207: * Constructor that read all binding annotations of the components inside the specified desktop.
208: * @param desktop the ZUML desktop.
209: */
210: public AnnotateDataBinder(Desktop desktop) {
211: this (desktop, true);
212: }
213:
214: /**
215: * Constructor that read all binding annotations of the components inside the specified page.
216: * @param page the ZUML page.
217: */
218: public AnnotateDataBinder(Page page) {
219: this (page, true);
220: }
221:
222: /**
223: * Constructor that read all binding annotations in the components inside the specified component (inclusive).
224: * @param comp the ZUML component.
225: */
226: public AnnotateDataBinder(Component comp) {
227: this (comp, true);
228: }
229:
230: /**
231: * Constructor that read all binding annotations of the given components array.
232: * @param comps the Component array.
233: * @since 3.0.0
234: */
235: public AnnotateDataBinder(Component[] comps) {
236: this (comps, true);
237: }
238:
239: /**
240: * Constructor that read all binding annotations of the components inside the specified desktop.
241: * @param desktop the ZUML desktop.
242: * @param defaultConfig whether load default binding configuration defined in lang-addon.xml
243: */
244: public AnnotateDataBinder(Desktop desktop, boolean defaultConfig) {
245: setDefaultConfig(defaultConfig);
246: for (final Iterator it = desktop.getComponents().iterator(); it
247: .hasNext();) {
248: loadAnnotations((Component) it.next());
249: }
250: }
251:
252: /**
253: * Constructor that read all binding annotations of the components inside the specified page.
254: * @param page the ZUML page.
255: * @param defaultConfig whether load default binding configuration defined in lang-addon.xml
256: */
257: public AnnotateDataBinder(Page page, boolean defaultConfig) {
258: setDefaultConfig(defaultConfig);
259: for (final Iterator it = page.getRoots().iterator(); it
260: .hasNext();) {
261: loadAnnotations((Component) it.next());
262: }
263: }
264:
265: /**
266: * Constructor that read all binding annotations of the given component array.
267: * @param comps the Component array
268: * @param defaultConfig whether load default binding configuration defined in lang-addon.xml
269: * @since 3.0.0
270: */
271: public AnnotateDataBinder(Component[] comps, boolean defaultConfig) {
272: setDefaultConfig(defaultConfig);
273: for (int j = 0; j < comps.length; ++j) {
274: loadAnnotations(comps[j]);
275: }
276: }
277:
278: /**
279: * Constructor that read all binding annotations in the components inside the specified component (inclusive).
280: * @param comp the ZUML component.
281: * @param defaultConfig whether load default binding configuration defined in lang-addon.xml
282: */
283: public AnnotateDataBinder(Component comp, boolean defaultConfig) {
284: setDefaultConfig(defaultConfig);
285: loadAnnotations(comp);
286: }
287:
288: private void loadAnnotations(Component comp) {
289: loadComponentAnnotation(comp);
290: loadComponentPropertyAnnotation(comp);
291:
292: final List children = comp.getChildren();
293: for (final Iterator it = children.iterator(); it.hasNext();) {
294: loadAnnotations((Component) it.next()); //recursive back
295: }
296: }
297:
298: private void loadComponentPropertyAnnotation(Component comp) {
299: loadComponentPropertyAnnotationByAnnotName(comp, "default");
300: loadComponentPropertyAnnotationByAnnotName(comp, "bind");
301: }
302:
303: private void loadComponentPropertyAnnotationByAnnotName(
304: Component comp, String annotName) {
305: ComponentCtrl compCtrl = (ComponentCtrl) comp;
306: final List props = compCtrl.getAnnotatedPropertiesBy(annotName);
307: for (final Iterator it = props.iterator(); it.hasNext();) {
308: final String propName = (String) it.next();
309: //[0] value, [1] loadWhenEvents, [2] saveWhenEvents, [3] access, [4] converter
310: final Object[] objs = loadPropertyAnnotation(comp,
311: propName, annotName);
312: addBinding(comp, propName, (String) objs[0],
313: (List) objs[1], (List) objs[2], (String) objs[3],
314: (String) objs[4]);
315: }
316: }
317:
318: private void loadComponentAnnotation(Component comp) {
319: loadComponentAnnotation(comp, "default");
320: loadComponentAnnotation(comp, "bind");
321: }
322:
323: private void loadComponentAnnotation(Component comp,
324: String annotName) {
325: ComponentCtrl compCtrl = (ComponentCtrl) comp;
326: Annotation ann = compCtrl.getAnnotation(annotName);
327: if (ann != null) {
328: Map attrs = ann.getAttributes();
329: for (final Iterator it = attrs.entrySet().iterator(); it
330: .hasNext();) {
331: Entry me = (Entry) it.next();
332: String attr = (String) me.getKey();
333: //[0] bean value, [1 ~ *] tag:expression
334: List expr = parseExpression((String) me.getValue(), ";");
335: if (expr == null || expr.get(0) == null) {
336: throw new UiException(
337: "Cannot find any bean value in the annotation <a:bind "
338: + attr + "=\"\"/> for component "
339: + comp + ", id=" + comp.getId());
340: } else {
341: List tags = parseExpression((String) expr.get(0),
342: ":");
343: if (tags.size() > 1) {
344: throw new UiException(
345: "bean value must be defined as the first statement in the annotation <a:bind "
346: + attr
347: + "=\"\"/> for component "
348: + comp + ", id=" + comp.getId());
349: }
350: }
351:
352: List loadWhenEvents = null;
353: List saveWhenEvents = null;
354: String access = null;
355: String converter = null;
356:
357: //process tags
358: for (int j = 1; j < expr.size(); ++j) {
359: List tags = parseExpression((String) expr.get(j),
360: ":");
361: if (tags == null) {
362: continue; //skip
363: }
364: if ("load-when".equals(tags.get(0))) {
365: if (tags.size() > 1 && tags.get(1) != null) {
366: loadWhenEvents = parseExpression(
367: (String) tags.get(1), ",");
368: } else {
369: loadWhenEvents.add(NULLIFY);
370: }
371: } else if ("save-when".equals(tags.get(0))) {
372: if (tags.size() > 1 && tags.get(1) != null) {
373: saveWhenEvents = parseExpression(
374: (String) tags.get(1), ",");
375: } else {
376: saveWhenEvents.add(NULLIFY);
377: }
378: } else if ("access".equals(tags.get(0))) {
379: access = tags.size() > 1 ? (String) tags.get(1)
380: : NULLIFY;
381: } else if ("converter".equals(tags.get(0))) {
382: converter = tags.size() > 1 ? (String) tags
383: .get(1) : NULLIFY;
384: }
385: }
386:
387: if (loadWhenEvents != null && loadWhenEvents.isEmpty()) {
388: loadWhenEvents = null;
389: }
390: if (saveWhenEvents != null && saveWhenEvents.isEmpty()) {
391: saveWhenEvents = null;
392: }
393:
394: addBinding(comp, attr, (String) expr.get(0),
395: loadWhenEvents, saveWhenEvents, access,
396: converter);
397: }
398: }
399: }
400: }
|