001: /*
002: * Licensed to the Apache Software Foundation (ASF) under one or more
003: * contributor license agreements. See the NOTICE file distributed with
004: * this work for additional information regarding copyright ownership.
005: * The ASF licenses this file to You under the Apache License, Version 2.0
006: * (the "License"); you may not use this file except in compliance with
007: * the License. You may obtain a copy of the License at
008: *
009: * http://www.apache.org/licenses/LICENSE-2.0
010: *
011: * Unless required by applicable law or agreed to in writing, software
012: * distributed under the License is distributed on an "AS IS" BASIS,
013: * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014: * See the License for the specific language governing permissions and
015: * limitations under the License.
016: */
017:
018: package org.apache.cocoon.forms.generation;
019:
020: import java.util.Collections;
021: import java.util.HashMap;
022: import java.util.Iterator;
023: import java.util.Locale;
024: import java.util.Map;
025: import java.util.Set;
026:
027: import org.apache.cocoon.ajax.BrowserUpdateTransformer;
028: import org.apache.cocoon.environment.Request;
029: import org.apache.cocoon.forms.FormsConstants;
030: import org.apache.cocoon.forms.FormsRuntimeException;
031: import org.apache.cocoon.forms.event.ValueChangedListenerEnabled;
032: import org.apache.cocoon.forms.formmodel.Form;
033: import org.apache.cocoon.forms.formmodel.Repeater;
034: import org.apache.cocoon.forms.formmodel.Widget;
035: import org.apache.cocoon.forms.formmodel.tree.Tree;
036: import org.apache.cocoon.forms.formmodel.tree.TreeWalker;
037: import org.apache.cocoon.forms.validation.ValidationError;
038: import org.apache.cocoon.i18n.I18nUtils;
039: import org.apache.cocoon.xml.AbstractXMLPipe;
040: import org.apache.cocoon.xml.AttributesImpl;
041: import org.apache.cocoon.xml.XMLConsumer;
042: import org.apache.cocoon.xml.XMLUtils;
043: import org.apache.commons.collections.ArrayStack;
044: import org.apache.commons.lang.BooleanUtils;
045: import org.xml.sax.Attributes;
046: import org.xml.sax.ContentHandler;
047: import org.xml.sax.SAXException;
048:
049: /**
050: * Helper class for the implementation of the CForms template language with JXTemplate macros.
051: *
052: * @version $Id: JXMacrosHelper.java 497100 2007-01-17 17:53:33Z bruno $
053: */
054: public class JXMacrosHelper {
055:
056: private XMLConsumer cocoonConsumer;
057: private Request request;
058: private Locale locale;
059: private ArrayStack widgetStack = new ArrayStack();
060: private ArrayStack pipeStack = new ArrayStack();
061: private Map classes; // lazily created
062: private boolean ajaxRequest;
063: private boolean ajaxTemplate;
064: private Set updatedWidgets;
065: private Set childUpdatedWidgets;
066:
067: /**
068: * Builds and helper object, given the generator's consumer.
069: *
070: * @param consumer the generator's consumer
071: * @return a helper object
072: */
073: public static JXMacrosHelper createHelper(XMLConsumer consumer,
074: Request request, String locale) {
075: return new JXMacrosHelper(consumer, request, locale);
076: }
077:
078: public JXMacrosHelper(XMLConsumer consumer, Request request,
079: String locale) {
080: this .cocoonConsumer = consumer;
081: this .request = request;
082: this .locale = I18nUtils.parseLocale(locale);
083: this .ajaxRequest = request.getParameter("cocoon-ajax") != null;
084: }
085:
086: public Form getForm(Form form, String attributeName) {
087: Form returnForm = form;
088: // if there hasn't been passed a form object, try to find it in the request
089: if (returnForm == null) {
090: returnForm = (Form) this .request
091: .getAttribute(attributeName);
092: }
093: if (returnForm != null) {
094: return returnForm;
095: }
096: throw new FormsRuntimeException(
097: "The template cannot find a form object");
098: }
099:
100: public void startForm(Form form, Map attributes)
101: throws SAXException {
102:
103: this .updatedWidgets = form.getUpdatedWidgetIds();
104: this .childUpdatedWidgets = form.getChildUpdatedWidgetIds();
105:
106: // build attributes
107: AttributesImpl attrs = new AttributesImpl();
108: // top-level widget-containers like forms might have their id set to ""
109: // for those the @id should not be included.
110: if (form.getId().length() != 0) {
111: attrs.addCDATAAttribute("id", form
112: .getRequestParameterName());
113: }
114:
115: // Add the "state" attribute
116: attrs.addCDATAAttribute("state", form.getCombinedState()
117: .getName());
118:
119: // Add locale attribute, useful for client-side code which needs to do stuff that
120: // corresponds to the form locale (e.g. date pickers)
121: attrs.addCDATAAttribute("locale", this .locale.toString()
122: .replaceAll("_", "-"));
123:
124: // Add the "listening" attribute is the value has change listeners
125: if (form instanceof ValueChangedListenerEnabled
126: && ((ValueChangedListenerEnabled) form)
127: .hasValueChangedListeners()) {
128: attrs.addCDATAAttribute("listening", "true");
129: }
130: Iterator iter = attributes.entrySet().iterator();
131: while (iter.hasNext()) {
132: Map.Entry entry = (Map.Entry) iter.next();
133: final String attrName = (String) entry.getKey();
134: // check if the attribute has already been defined
135: if (attrs.getValue(attrName) != null) {
136: attrs.removeAttribute(attrName);
137: }
138: attrs
139: .addCDATAAttribute(attrName, (String) entry
140: .getValue());
141: }
142:
143: this .ajaxTemplate = "true".equals(attributes.get("ajax"));
144:
145: this .cocoonConsumer.startPrefixMapping(
146: FormsConstants.INSTANCE_PREFIX,
147: FormsConstants.INSTANCE_NS);
148: this .cocoonConsumer.startElement(FormsConstants.INSTANCE_NS,
149: "form-template", FormsConstants.INSTANCE_PREFIX_COLON
150: + "form-template", attrs);
151: // Push the form at the top of the stack
152: this .widgetStack.push(Boolean.FALSE); // Not in an updated template
153: this .widgetStack.push(form);
154: }
155:
156: public void endForm() throws SAXException {
157: this .widgetStack.pop();
158: this .widgetStack.pop();
159: this .cocoonConsumer.endElement(FormsConstants.INSTANCE_NS,
160: "form-template", FormsConstants.INSTANCE_PREFIX_COLON
161: + "form-template");
162: this .cocoonConsumer
163: .endPrefixMapping(FormsConstants.INSTANCE_PREFIX);
164:
165: this .ajaxTemplate = false;
166: this .updatedWidgets = null;
167: }
168:
169: private void startBuReplace(String id) throws SAXException {
170: AttributesImpl attr = new AttributesImpl();
171: attr.addCDATAAttribute("id", id);
172: this .cocoonConsumer.startElement(
173: BrowserUpdateTransformer.BU_NSURI, "replace",
174: "bu:replace", attr);
175: }
176:
177: private void endBuReplace(String id) throws SAXException {
178: this .cocoonConsumer.endElement(
179: BrowserUpdateTransformer.BU_NSURI, "replace",
180: "bu:replace");
181: }
182:
183: protected boolean pushWidget(String path, boolean unused)
184: throws SAXException {
185: Widget parent = peekWidget();
186: if (path == null || path.length() == 0) {
187: throw new FormsRuntimeException(
188: "Missing 'id' attribute on template instruction");
189: }
190: Widget widget = parent.lookupWidget(path);
191: if (widget == null) {
192: throw new FormsRuntimeException(parent
193: + " has no child named '" + path + "'", parent
194: .getLocation());
195: }
196:
197: String id = widget.getFullName();
198: // Is there an updated widget at a higher level in the template?
199: boolean inUpdatedTemplate = ((Boolean) widgetStack.peek(1))
200: .booleanValue();
201:
202: boolean display;
203:
204: if (ajaxRequest) {
205: // An Ajax request. We will send partial updates
206: if (inUpdatedTemplate) {
207: // A parent widget has been updated: redisplay this one also
208: display = true;
209: } else if (this .updatedWidgets.contains(id)) {
210: // Widget has been updated. We are now in an updated template section,
211: // and widgets have to be surrounded with <bu:replace>
212: inUpdatedTemplate = true;
213: display = true;
214: } else if (this .childUpdatedWidgets.contains(id)) {
215: // A child need to be updated
216: display = true;
217: } else {
218: // Doesn't need to be displayed
219: display = false;
220: }
221: } else {
222: // Not an ajax request
223: if (ajaxTemplate) {
224: // Surround all widgets with <bu:replace>, which the bu tranformer will use to check structure
225: // consistency and add an id attribute to its child elements.
226: inUpdatedTemplate = true;
227: }
228: // Display the widget
229: display = true;
230: }
231:
232: if (display) {
233: // Widget needs to be displayed, but does it actually allows it?
234: if (widget.getState().isDisplayingValues()) {
235: if (inUpdatedTemplate) {
236: // Updated part of an Ajax template: surround with <bu:replace>
237: startBuReplace(id);
238: }
239: } else {
240: if (ajaxTemplate) {
241: // Generate a placeholder, so that the page can be updated later
242: startBuReplace(id);
243: AttributesImpl attrs = new AttributesImpl();
244: attrs.addCDATAAttribute("id", id);
245: this .cocoonConsumer.startElement(
246: FormsConstants.INSTANCE_NS, "placeholder",
247: FormsConstants.INSTANCE_PREFIX_COLON
248: + "placeholder", attrs);
249: this .cocoonConsumer.endElement(
250: FormsConstants.INSTANCE_NS, "placeholder",
251: FormsConstants.INSTANCE_PREFIX_COLON
252: + "placeholder");
253: endBuReplace(id);
254: }
255: // Production finished for this widget
256: display = false;
257: }
258: }
259:
260: if (display) {
261: this .widgetStack.push(BooleanUtils
262: .toBooleanObject(inUpdatedTemplate));
263: this .widgetStack.push(widget);
264: }
265:
266: return display;
267: }
268:
269: public Widget peekWidget() {
270: return (Widget) this .widgetStack.peek();
271: }
272:
273: public void popWidget() throws SAXException {
274: Widget widget = (Widget) this .widgetStack.pop();
275: boolean inUpdatedTemplate = ((Boolean) this .widgetStack.pop())
276: .booleanValue();
277:
278: if (inUpdatedTemplate) {
279: // Close the bu:replace
280: endBuReplace(widget.getFullName());
281: }
282: }
283:
284: public boolean pushWidget(String path) throws SAXException {
285: return pushWidget(path, false);
286: }
287:
288: public boolean pushContainer(String path) throws SAXException {
289: return pushWidget(path, true);
290: }
291:
292: /**
293: * Enter a repeater
294: *
295: * @param path widget path
296: * @param ajaxAware distinguishes between <ft:repeater-widget> and <ft:repeater>.
297: * @return true if the repeater template is to be executed
298: * @throws SAXException
299: */
300: public boolean pushRepeater(String path, boolean ajaxAware)
301: throws SAXException {
302: if (!ajaxAware && this .ajaxTemplate) {
303: throw new IllegalStateException(
304: "Cannot use <ft:repeater-widget> in an Ajax form");
305: }
306: boolean result = pushWidget(path, true);
307: if (result && !(peekWidget() instanceof Repeater)) {
308: throw new IllegalArgumentException("Widget " + peekWidget()
309: + " is not a repeater");
310: }
311: return result;
312: }
313:
314: /**
315: * Get a child widget of a given widget, throwing an exception if no such child exists.
316: *
317: * @param currentWidget
318: * @param path
319: */
320: public Widget getWidget(Widget currentWidget, String path) {
321: Widget result = currentWidget.lookupWidget(path);
322:
323: if (result != null) {
324: return result;
325: }
326: throw new FormsRuntimeException(currentWidget
327: + " has no child named '" + path + "'", currentWidget
328: .getLocation());
329: }
330:
331: private Repeater getRepeater(Widget currentWidget, String id) {
332: Widget child = getWidget(currentWidget, id);
333: if (child instanceof Repeater) {
334: return (Repeater) child;
335: }
336: throw new FormsRuntimeException(child + " is not a repeater",
337: child.getLocation());
338: }
339:
340: /**
341: * Generate a widget's SAX fragment, buffering the root element's <code>endElement()</code>
342: * event so that the template can insert styling information in it.
343: *
344: * @param widget
345: * @param arguments
346: * @throws SAXException
347: */
348: public void generateWidget(Widget widget, Map arguments)
349: throws SAXException {
350: // Needs to be buffered
351: RootBufferingPipe pipe = new RootBufferingPipe(
352: this .cocoonConsumer, arguments);
353: this .pipeStack.push(pipe);
354: widget.generateSaxFragment(pipe, this .locale);
355: }
356:
357: /**
358: * Flush the root element name that has been stored in
359: * {@link #generateWidget(Widget, Map)}.
360: *
361: * @throws SAXException
362: */
363: public void flushRootAndPop() throws SAXException {
364: ((RootBufferingPipe) pipeStack.pop()).flushRoot();
365: popWidget();
366: }
367:
368: public void flushRoot() throws SAXException {
369: ((RootBufferingPipe) pipeStack.pop()).flushRoot();
370: }
371:
372: public void generateWidgetLabel(Widget widget, String id)
373: throws SAXException {
374: getWidget(widget, id).generateLabel(this .cocoonConsumer);
375: }
376:
377: public void generateRepeaterWidgetLabel(Widget widget, String id,
378: String widgetId) throws SAXException {
379: // Widget labels are allowed either inside or outside of <ft:repeater>
380: Repeater repeater = widget instanceof Repeater ? (Repeater) widget
381: : getRepeater(widget, id);
382: repeater.generateWidgetLabel(widgetId, this .cocoonConsumer);
383: }
384:
385: public void generateRepeaterSize(Widget widget, String id)
386: throws SAXException {
387: getRepeater(widget, id).generateSize(this .cocoonConsumer);
388: }
389:
390: private static final String VALIDATION_ERROR = "validation-error";
391:
392: public void generateValidationError(ValidationError error)
393: throws SAXException {
394: // Needs to be buffered
395: RootBufferingPipe pipe = new RootBufferingPipe(
396: this .cocoonConsumer);
397: this .pipeStack.push(pipe);
398: pipe
399: .startElement(FormsConstants.INSTANCE_NS,
400: VALIDATION_ERROR,
401: FormsConstants.INSTANCE_PREFIX_COLON
402: + VALIDATION_ERROR,
403: XMLUtils.EMPTY_ATTRIBUTES);
404: error.generateSaxFragment(pipe);
405: pipe
406: .endElement(FormsConstants.INSTANCE_NS,
407: VALIDATION_ERROR,
408: FormsConstants.INSTANCE_PREFIX_COLON
409: + VALIDATION_ERROR);
410: }
411:
412: public boolean isValidationError(Object object) {
413: return object instanceof ValidationError;
414: }
415:
416: public void defineClassBody(Form form, String id, Object body) {
417: // TODO: check that class actually exists in the form
418: if (this .classes == null) {
419: this .classes = new HashMap();
420: }
421:
422: // TODO: check if class doesn't already exist?
423: this .classes.put(id, body);
424: }
425:
426: public Object getClassBody(String id) {
427: Object result = this .classes == null ? null : this .classes
428: .get(id);
429:
430: if (result == null) {
431: throw new FormsRuntimeException("No class '" + id
432: + "' has been defined.");
433: }
434: return result;
435: }
436:
437: public boolean isSelectedCase(Widget unionWidget, String caseValue) {
438: String value = (String) unionWidget.getValue();
439: return caseValue.equals(value != null ? value : "");
440: }
441:
442: public TreeWalker createWalker() {
443: return new TreeWalker((Tree) peekWidget());
444: }
445:
446: public boolean isVisible(Widget widget) throws SAXException {
447: boolean visible = widget.getCombinedState()
448: .isDisplayingValues();
449:
450: if (!visible) {
451: // Generate a placeholder it not visible
452: String id = widget.getRequestParameterName();
453: AttributesImpl attrs = new AttributesImpl();
454: attrs.addCDATAAttribute("id", id);
455: this .cocoonConsumer.startElement(
456: BrowserUpdateTransformer.BU_NSURI, "replace",
457: "bu:replace", attrs);
458: this .cocoonConsumer.startElement(
459: FormsConstants.INSTANCE_NS, "placeholder",
460: FormsConstants.INSTANCE_PREFIX_COLON
461: + "placeholder", attrs);
462: this .cocoonConsumer.endElement(FormsConstants.INSTANCE_NS,
463: "placeholder", FormsConstants.INSTANCE_PREFIX_COLON
464: + "placeholder");
465: this .cocoonConsumer.endElement(
466: BrowserUpdateTransformer.BU_NSURI, "replace",
467: "bu:replace");
468: }
469:
470: return visible;
471: }
472:
473: public boolean isModified(Widget widget) {
474: return this .updatedWidgets.contains(widget
475: .getRequestParameterName());
476: }
477:
478: public boolean generateStyling(Map attributes) throws SAXException {
479: return generateStyling(this .cocoonConsumer, attributes);
480: }
481:
482: /**
483: * Generate a <code><fi:styling></code> element holding the attributes of a <code>ft:*</code>
484: * element that are in the "fi:" namespace.
485: *
486: * @param attributes the template instruction attributes
487: * @return true if a <code><fi:styling></code> was produced
488: * @throws SAXException
489: */
490: public static boolean generateStyling(ContentHandler handler,
491: Map attributes) throws SAXException {
492: AttributesImpl attr = null;
493: Iterator entries = attributes.entrySet().iterator();
494: while (entries.hasNext()) {
495: Map.Entry entry = (Map.Entry) entries.next();
496: String key = (String) entry.getKey();
497:
498: // FIXME: JXTG only gives the local name of attributes, so we can't distinguish namespaces...
499: if (!"id".equals(key) && !"widget-id".equals(key)) {
500: if (attr == null)
501: attr = new AttributesImpl();
502: attr.addCDATAAttribute(key, (String) entry.getValue());
503: }
504: }
505:
506: if (attr != null) {
507: // There were some styling attributes
508: handler.startElement(FormsConstants.INSTANCE_NS, "styling",
509: FormsConstants.INSTANCE_PREFIX_COLON + "styling",
510: attr);
511: handler.endElement(FormsConstants.INSTANCE_NS, "styling",
512: FormsConstants.INSTANCE_PREFIX_COLON + "styling");
513: return true;
514: } else {
515: return false;
516: }
517: }
518:
519: /**
520: * A SAX pipe that buffers the <code>endElement()</code> event of the root element.
521: * This is needed by the generator version of the FormsTransformer (see jx-macros.xml).
522: *
523: * @version $Id: JXMacrosHelper.java 497100 2007-01-17 17:53:33Z bruno $
524: */
525: private static class RootBufferingPipe extends AbstractXMLPipe {
526: private int depth = 0;
527:
528: private String rootUri;
529: private String rootLoc;
530: private String rootRaw;
531: private Map arguments;
532: private boolean forbidStyling = false;
533:
534: public RootBufferingPipe(XMLConsumer next) {
535: this (next, Collections.EMPTY_MAP);
536: }
537:
538: public RootBufferingPipe(XMLConsumer next, Map arguments) {
539: this .setConsumer(next);
540: this .arguments = arguments;
541: }
542:
543: public void startElement(String uri, String loc, String raw,
544: Attributes a) throws SAXException {
545: super .startElement(uri, loc, raw, a);
546: if (depth == 0) {
547: // Root element: keep its description
548: this .rootUri = uri;
549: this .rootLoc = loc;
550: this .rootRaw = raw;
551:
552: // And produce fi:styling from attributes
553: this .forbidStyling = generateStyling(
554: this .contentHandler, arguments);
555: }
556:
557: if (depth == 1 && forbidStyling
558: && uri.equals(FormsConstants.INSTANCE_NS)
559: && loc.equals("styling")) {
560: throw new SAXException(
561: "Cannot use 'fi:*' attributes and <fi:styling> at the same time");
562: }
563:
564: depth++;
565: }
566:
567: public void endElement(String uri, String loc, String raw)
568: throws SAXException {
569: depth--;
570: if (depth > 0) {
571: // Propagate all but root element
572: super .endElement(uri, loc, raw);
573: }
574: }
575:
576: public void flushRoot() throws SAXException {
577: if (depth != 0) {
578: throw new IllegalStateException("Depth is not zero");
579: }
580: super.endElement(this.rootUri, this.rootLoc, this.rootRaw);
581: }
582: }
583: }
|