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: package org.apache.cocoon.forms.formmodel;
018:
019: import java.util.ArrayList;
020: import java.util.HashMap;
021: import java.util.Iterator;
022: import java.util.List;
023: import java.util.Locale;
024: import java.util.Map;
025:
026: import org.apache.cocoon.forms.FormsConstants;
027: import org.apache.cocoon.forms.event.CreateEvent;
028: import org.apache.cocoon.forms.event.ValueChangedListenerEnabled;
029: import org.apache.cocoon.forms.event.WidgetEvent;
030: import org.apache.cocoon.forms.validation.ValidationErrorAware;
031: import org.apache.cocoon.forms.validation.WidgetValidator;
032: import org.apache.cocoon.util.location.Location;
033: import org.apache.cocoon.xml.AttributesImpl;
034:
035: import org.xml.sax.ContentHandler;
036: import org.xml.sax.SAXException;
037:
038: /**
039: * Abstract base class for Widget implementations. Provides functionality
040: * common to many widgets.
041: *
042: * @version $Id: AbstractWidget.java 449149 2006-09-23 03:58:05Z crossley $
043: */
044: public abstract class AbstractWidget implements Widget {
045:
046: /**
047: * Containing parent-widget to this widget.
048: * NOTE: avoid directly accessing this member since subclasses can mask this
049: * property through own implemented getParent()
050: */
051: private Widget parent;
052:
053: /**
054: * The widget's own state
055: */
056: private WidgetState state = WidgetState.ACTIVE;
057:
058: /**
059: * Lazy loaded reference to the top-level form.
060: */
061: private Form form;
062:
063: /**
064: * Validation-rules local to the widget instance
065: */
066: private List validators;
067:
068: /**
069: * Storage for the widget allocated attributes
070: */
071: private Map attributes;
072:
073: /**
074: * The result of the last call to {@link #validate()}.
075: */
076: protected boolean wasValid = true;
077:
078: protected AbstractWidget(AbstractWidgetDefinition definition) {
079: this .state = definition.getState();
080: }
081:
082: /**
083: * Called after widget's environment has been setup,
084: * to allow for any contextual initalization, such as
085: * looking up case widgets for union widgets.
086: */
087: public void initialize() {
088: ((AbstractWidgetDefinition) getDefinition())
089: .widgetCreated(this );
090: }
091:
092: /**
093: * Gets the id of this widget.
094: */
095: public String getId() {
096: return getDefinition().getId();
097: }
098:
099: public String getName() {
100: return getId();
101: }
102:
103: /**
104: * Concrete subclasses should allow access to their underlaying Definition
105: * through this method.
106: *
107: * If subclasses decide to return <code>null</code> they should also organize
108: * own implementations of {@link #getId()}, {@link #getLocation()},
109: * {@link #validate()}, {@link #generateLabel(ContentHandler)} and
110: * {@link #generateDisplayData(ContentHandler)} to avoid NPE's.
111: *
112: * @return the widgetDefinition from which this widget was instantiated.
113: * (See {@link org.apache.cocoon.forms.formmodel.WidgetDefinition#createInstance()})
114: */
115: public abstract WidgetDefinition getDefinition();
116:
117: /**
118: * @return the location-information (file, line and column) where this widget was
119: * configured.
120: */
121: public Location getLocation() {
122: return getDefinition().getLocation();
123: }
124:
125: /**
126: * @return The parent-widget of this widget.
127: */
128: // This method is final in order for other methods in this class to use this.parent
129: public final Widget getParent() {
130: return this .parent;
131: }
132:
133: /**
134: * Sets the parent-widget of this widget.
135: * This is a write-once property.
136: *
137: * @param widget the parent-widget of this one.
138: * @throws IllegalStateException when the parent had already been set.
139: */
140: public void setParent(Widget widget) {
141: if (this .parent != null) {
142: throw new IllegalStateException("The parent of widget "
143: + getRequestParameterName()
144: + " should only be set once.");
145: }
146: this .parent = widget;
147: }
148:
149: /**
150: * @return the form where this widget belongs to.
151: */
152: public Form getForm() {
153: if (this .form == null) {
154: Widget myParent = getParent();
155: if (myParent == null) {
156: this .form = (Form) this ;
157: } else {
158: this .form = myParent.getForm();
159: }
160: }
161: return this .form;
162: }
163:
164: public WidgetState getState() {
165: return this .state;
166: }
167:
168: public void setState(WidgetState state) {
169: if (state == null) {
170: throw new IllegalArgumentException(
171: "A widget state cannot be set to null");
172: }
173: this .state = state;
174:
175: // Update the browser
176: getForm().addWidgetUpdate(this );
177: }
178:
179: public WidgetState getCombinedState() {
180: if (this .parent == null) {
181: return this .state;
182: }
183: return WidgetState.strictest(this .state, this .parent
184: .getCombinedState());
185: }
186:
187: // Cached param names, used to speed up execution of the method below while
188: // still allowing ids to change (e.g. repeater rows when they are reordered).
189: private String cachedParentParamName;
190: private String cachedParamName;
191:
192: /**
193: * Should be called when a widget's own name has changed, in order to clear
194: * internal caches used to compute request parameters.
195: */
196: protected void widgetNameChanged() {
197: this .cachedParentParamName = null;
198: this .cachedParamName = null;
199: }
200:
201: public String getFullName() {
202: return getRequestParameterName();
203: }
204:
205: public String getRequestParameterName() {
206:
207: if (this .parent == null) {
208: return getId();
209: }
210:
211: String parentParamName = parent.getRequestParameterName();
212: if (parentParamName.equals(this .cachedParentParamName)) {
213: // Parent name hasn't changed, so ours hasn't changed too
214: return this .cachedParamName;
215: }
216:
217: // Compute our name and cache it
218: this .cachedParentParamName = parentParamName;
219: if (this .cachedParentParamName.length() == 0) {
220: // the top level form returns an id == ""
221: this .cachedParamName = getId();
222: } else {
223: this .cachedParamName = this .cachedParentParamName + "."
224: + getId();
225: }
226:
227: return this .cachedParamName;
228: }
229:
230: public Widget lookupWidget(String path) {
231:
232: if (path == null || path.length() == 0) {
233: return this ;
234: }
235:
236: Widget relativeWidget;
237: String relativePath;
238: int sepPosition = path.indexOf("" + Widget.PATH_SEPARATOR);
239:
240: if (sepPosition < 0) {
241: //last step
242: if (path.startsWith(".."))
243: return getParent();
244: return getChild(path);
245: } else if (sepPosition == 0) {
246: //absolute path
247: relativeWidget = getForm();
248: relativePath = path.substring(1);
249: } else {
250: if (path.startsWith(".." + Widget.PATH_SEPARATOR)) {
251: relativeWidget = getParent();
252: relativePath = path.substring(3);
253: } else {
254: String childId = path.substring(0, sepPosition);
255: relativeWidget = getChild(childId);
256: relativePath = path.substring(sepPosition + 1);
257: }
258: }
259:
260: if (relativeWidget == null) {
261: return null;
262: }
263:
264: return relativeWidget.lookupWidget(relativePath);
265: }
266:
267: /**
268: * Concrete widgets that contain actual child widgets should override to
269: * return the actual child-widget.
270: *
271: * @param id of the child-widget
272: * @return <code>null</code> if not overriden.
273: */
274: protected Widget getChild(String id) {
275: return null;
276: }
277:
278: public Widget getWidget(String id) {
279: throw new UnsupportedOperationException(
280: "getWidget(id) got deprecated from the API. \n"
281: + "Consider using getChild(id) or even lookupWidget(path) instead.");
282: }
283:
284: public Object getValue() {
285: throw new UnsupportedOperationException("Widget " + this
286: + " has no value, at " + getLocation());
287: }
288:
289: public void setValue(Object object) {
290: throw new UnsupportedOperationException("Widget " + this
291: + " has no value, at " + getLocation());
292: }
293:
294: public boolean isRequired() {
295: return false;
296: }
297:
298: /**
299: * {@inheritDoc}
300: *
301: * Abstract implementation throws a {@link UnsupportedOperationException}.
302: * Concrete subclass widgets need to override when supporting event broadcasting.
303: */
304: public void broadcastEvent(WidgetEvent event) {
305: if (event instanceof CreateEvent) {
306: ((AbstractWidgetDefinition) getDefinition())
307: .fireCreateEvent((CreateEvent) event);
308: } else {
309: throw new UnsupportedOperationException("Widget "
310: + getRequestParameterName()
311: + " doesn't handle events.");
312: }
313: }
314:
315: /**
316: * Add a validator to this widget instance.
317: *
318: * @param validator
319: */
320: public void addValidator(WidgetValidator validator) {
321: if (this .validators == null) {
322: this .validators = new ArrayList();
323: }
324: this .validators.add(validator);
325: }
326:
327: /**
328: * Remove a validator from this widget instance
329: *
330: * @param validator
331: * @return <code>true</code> if the validator was found.
332: */
333: public boolean removeValidator(WidgetValidator validator) {
334: if (this .validators != null) {
335: return this .validators.remove(validator);
336: }
337: return false;
338: }
339:
340: /**
341: * @see org.apache.cocoon.forms.formmodel.Widget#validate()
342: */
343: public boolean validate() {
344: // Consider widget valid if it is not validating values.
345: if (!getCombinedState().isValidatingValues()) {
346: this .wasValid = true;
347: return true;
348: }
349:
350: // Test validators from the widget definition
351: if (!getDefinition().validate(this )) {
352: // Failed
353: this .wasValid = false;
354: return false;
355: }
356: // Definition successful, test local validators
357: if (this .validators != null) {
358: Iterator iter = this .validators.iterator();
359: while (iter.hasNext()) {
360: WidgetValidator validator = (WidgetValidator) iter
361: .next();
362: if (!validator.validate(this )) {
363: this .wasValid = false;
364: return false;
365: }
366: }
367: }
368:
369: // Successful validation
370:
371: if (this instanceof ValidationErrorAware) {
372: // Clear validation error if any
373: ((ValidationErrorAware) this ).setValidationError(null);
374: }
375:
376: this .wasValid = true;
377: return true;
378: }
379:
380: /**
381: * @see org.apache.cocoon.forms.formmodel.Widget#isValid()
382: */
383: public boolean isValid() {
384: return this .wasValid;
385: }
386:
387: /**
388: * {@inheritDoc}
389: *
390: * Delegates to the {@link #getDefinition()} to generate the 'label' part of
391: * the display-data of this widget.
392: *
393: * Subclasses should override if the getDefinition can return <code>null</code>
394: * to avoid NPE's
395: *
396: * @param contentHandler
397: * @throws SAXException
398: */
399: public void generateLabel(ContentHandler contentHandler)
400: throws SAXException {
401: if (getCombinedState().isDisplayingValues()) {
402: getDefinition()
403: .generateDisplayData("label", contentHandler);
404: }
405: }
406:
407: /**
408: * Generates nested additional content nested inside the main element for this
409: * widget which is generated by {@link #generateSaxFragment(ContentHandler, Locale)}
410: *
411: * The implementation on the AbstractWidget level inserts no additional XML.
412: * Subclasses need to override to insert widget specific content.
413: *
414: * @param contentHandler to send the SAX events to
415: * @param locale in which context potential content needs to be put.
416: * @throws SAXException
417: */
418: protected void generateItemSaxFragment(
419: ContentHandler contentHandler, Locale locale)
420: throws SAXException {
421: // Do nothing
422: }
423:
424: /**
425: * The XML element name used in {@link #generateSaxFragment(ContentHandler, Locale)}
426: * to produce the wrapping element for all the XML-instance-content of this Widget.
427: *
428: * @return the main elementname for this widget's sax-fragment.
429: */
430: protected abstract String getXMLElementName();
431:
432: /**
433: * The XML attributes used in {@link #generateSaxFragment(ContentHandler, Locale)}
434: * to be placed on the wrapping element for all the XML-instance-content of this Widget.
435: *
436: * This automatically adds @id={@link #getRequestParameterName()} to that element.
437: * Concrete subclasses should call super.getXMLElementAttributes and possibly
438: * add additional attributes.
439: *
440: * Note: the @id is not added for those widgets who's getId() returns <code>null</code>
441: * (e.g. top-level container widgets like 'form'). The contract of returning a non-null
442: * {@link AttributesImpl} is however maintained.
443: *
444: * @return the attributes for the main element for this widget's sax-fragment.
445: */
446: protected AttributesImpl getXMLElementAttributes() {
447: AttributesImpl attrs = new AttributesImpl();
448: // top-level widget-containers like forms might have their id set to ""
449: // for those the @id should not be included.
450: if (getId().length() != 0) {
451: attrs.addCDATAAttribute("id", getRequestParameterName());
452: }
453:
454: // Add the "state" attribute
455: attrs.addCDATAAttribute("state", getCombinedState().getName());
456:
457: // Add the "listening" attribute is the value has change listeners
458: if (this instanceof ValueChangedListenerEnabled
459: && ((ValueChangedListenerEnabled) this )
460: .hasValueChangedListeners()) {
461: attrs.addCDATAAttribute("listening", "true");
462: }
463: return attrs;
464: }
465:
466: /**
467: * Delegates to the {@link #getDefinition()} of this widget to generate a common
468: * set of 'display' data. (i.e. help, label, hint,...)
469: *
470: * Subclasses should override if the getDefinition can return <code>null</code>
471: * to avoid NPE's.
472: *
473: * @param contentHandler where to send the SAX events to.
474: * @throws SAXException
475: *
476: * @see WidgetDefinition#generateDisplayData(ContentHandler)
477: */
478: protected void generateDisplayData(ContentHandler contentHandler)
479: throws SAXException {
480: getDefinition().generateDisplayData(contentHandler);
481: }
482:
483: /**
484: * {@inheritDoc}
485: *
486: * This will generate some standard XML consisting of a simple wrapper
487: * element (name provided by {@link #getXMLElementName()}) with attributes
488: * (provided by {@link #getXMLElementAttributes()} around anything injected
489: * in by both {@link #generateDisplayData(ContentHandler)} and
490: * {@link #generateItemSaxFragment(ContentHandler, Locale)}.
491: *
492: * <pre>
493: * <fi:{@link #getXMLElementName()} {@link #getXMLElementAttributes()} >
494: * {@link #generateDisplayData(ContentHandler)} (i.e. help, label, ...)
495: *
496: * {@link #generateItemSaxFragment(ContentHandler, Locale)}
497: * </fi:{@link #getXMLElementName()} >
498: * </pre>
499: *
500: * @param contentHandler to send the SAX events to
501: * @param locale in which context potential content needs to be put.
502: * @throws SAXException
503: */
504: public void generateSaxFragment(ContentHandler contentHandler,
505: Locale locale) throws SAXException {
506:
507: if (getCombinedState().isDisplayingValues()) {
508: // FIXME: we may want to strip out completely widgets that aren't updated when in AJAX mode
509: String element = this .getXMLElementName();
510: AttributesImpl attrs = getXMLElementAttributes();
511: contentHandler.startElement(FormsConstants.INSTANCE_NS,
512: element, FormsConstants.INSTANCE_PREFIX_COLON
513: + element, attrs);
514:
515: generateDisplayData(contentHandler);
516:
517: if (locale == null) {
518: locale = getForm().getLocale();
519: }
520:
521: generateItemSaxFragment(contentHandler, locale);
522:
523: contentHandler.endElement(FormsConstants.INSTANCE_NS,
524: element, FormsConstants.INSTANCE_PREFIX_COLON
525: + element);
526:
527: } else {
528: // Generate a placeholder that can be used later by AJAX updates
529: AttributesImpl attrs = new AttributesImpl();
530: attrs.addCDATAAttribute("id", getRequestParameterName());
531: contentHandler.startElement(FormsConstants.INSTANCE_NS,
532: "placeholder", FormsConstants.INSTANCE_PREFIX_COLON
533: + "placeholder", attrs);
534: contentHandler.endElement(FormsConstants.INSTANCE_NS,
535: "placeholder", FormsConstants.INSTANCE_PREFIX_COLON
536: + "placeholder");
537: }
538: }
539:
540: public Object getAttribute(String name) {
541: Object result = null;
542:
543: // First check locally
544: if (this .attributes != null) {
545: result = this .attributes.get(name);
546: }
547:
548: // Fall back to the definition's attributes
549: if (result == null) {
550: result = getDefinition().getAttribute(name);
551: }
552:
553: return result;
554: }
555:
556: public void setAttribute(String name, Object value) {
557: if (this .attributes == null) {
558: this .attributes = new HashMap();
559: }
560: this .attributes.put(name, value);
561: }
562:
563: public void removeAttribute(String name) {
564: if (this .attributes != null) {
565: this .attributes.remove(name);
566: }
567: }
568:
569: public String toString() {
570: String className = this .getClass().getName();
571: int last = className.lastIndexOf('.');
572: if (last != -1) {
573: className = className.substring(last + 1);
574: }
575:
576: String name = getRequestParameterName();
577: return name.length() == 0 ? className : className + " '"
578: + getRequestParameterName() + "'";
579: }
580: }
|