001 // Copyright 2004, 2005 The Apache Software Foundation
002 //
003 // Licensed under the Apache License, Version 2.0 (the "License");
004 // you may not use this file except in compliance with the License.
005 // You may obtain a copy of the License at
006 //
007 // http://www.apache.org/licenses/LICENSE-2.0
008 //
009 // Unless required by applicable law or agreed to in writing, software
010 // distributed under the License is distributed on an "AS IS" BASIS,
011 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
012 // See the License for the specific language governing permissions and
013 // limitations under the License.
014
015 package org.apache.tapestry;
016
017 import java.util.Collection;
018 import java.util.Collections;
019 import java.util.HashMap;
020 import java.util.HashSet;
021 import java.util.Iterator;
022 import java.util.List;
023 import java.util.Map;
024
025 import org.apache.hivemind.ApplicationRuntimeException;
026 import org.apache.hivemind.Messages;
027 import org.apache.hivemind.impl.BaseLocatable;
028 import org.apache.hivemind.util.Defense;
029 import org.apache.hivemind.util.PropertyUtils;
030 import org.apache.tapestry.bean.BeanProvider;
031 import org.apache.tapestry.engine.IPageLoader;
032 import org.apache.tapestry.event.PageEvent;
033 import org.apache.tapestry.listener.ListenerMap;
034 import org.apache.tapestry.spec.IComponentSpecification;
035 import org.apache.tapestry.spec.IContainedComponent;
036
037 /**
038 * Abstract base class implementing the {@link IComponent}interface.
039 *
040 * @author Howard Lewis Ship
041 */
042
043 public abstract class AbstractComponent extends BaseLocatable implements IComponent
044 {
045 /**
046 * The page that contains the component, possibly itself (if the component is in fact, a page).
047 */
048
049 private IPage _page;
050
051 /**
052 * The component which contains the component. This will only be null if the component is
053 * actually a page.
054 */
055
056 private IComponent _container;
057
058 /**
059 * The simple id of this component.
060 */
061
062 private String _id;
063
064 /**
065 * The fully qualified id of this component. This is calculated the first time it is needed,
066 * then cached for later.
067 */
068
069 private String _idPath;
070
071 private static final int MAP_SIZE = 5;
072
073 /**
074 * A {@link Map}of all bindings (for which there isn't a corresponding JavaBeans property); the
075 * keys are the names of formal and informal parameters.
076 */
077
078 private Map _bindings;
079
080 private Map _components;
081
082 private static final int BODY_INIT_SIZE = 5;
083
084 private INamespace _namespace;
085
086 /**
087 * Used in place of JDK 1.3's Collections.EMPTY_MAP (which is not available in JDK 1.2).
088 */
089
090 private static final Map EMPTY_MAP = Collections.unmodifiableMap(new HashMap(1));
091
092 /**
093 * The number of {@link IRender}objects in the body of this component.
094 */
095
096 private int _bodyCount = 0;
097
098 /**
099 * An aray of elements in the body of this component.
100 */
101
102 private IRender[] _body;
103
104 /**
105 * The components' asset map.
106 */
107
108 private Map _assets;
109
110 /**
111 * A mapping that allows public instance methods to be dressed up as {@link IActionListener}
112 * listener objects.
113 *
114 * @since 1.0.2
115 */
116
117 private ListenerMap _listeners;
118
119 /**
120 * A bean provider; these are lazily created as needed.
121 *
122 * @since 1.0.4
123 */
124
125 private IBeanProvider _beans;
126
127 /**
128 * Returns true if the component is currently rendering.
129 *
130 * @see #prepareForRender(IRequestCycle)
131 * @see #cleanupAfterRender(IRequestCycle)
132 * @since 4.0
133 */
134
135 private boolean _rendering;
136
137 /**
138 * @since 4.0
139 */
140
141 private boolean _active;
142
143 /** @since 4.0 */
144
145 private IContainedComponent _containedComponent;
146
147 public void addAsset(String name, IAsset asset)
148 {
149 Defense.notNull(name, "name");
150 Defense.notNull(asset, "asset");
151
152 checkActiveLock();
153
154 if (_assets == null)
155 _assets = new HashMap(MAP_SIZE);
156
157 _assets.put(name, asset);
158 }
159
160 public void addComponent(IComponent component)
161 {
162 Defense.notNull(component, "component");
163
164 checkActiveLock();
165
166 if (_components == null)
167 _components = new HashMap(MAP_SIZE);
168
169 _components.put(component.getId(), component);
170 }
171
172 /**
173 * Adds an element (which may be static text or a component) as a body element of this
174 * component. Such elements are rendered by {@link #renderBody(IMarkupWriter, IRequestCycle)}.
175 *
176 * @since 2.2
177 */
178
179 public void addBody(IRender element)
180 {
181 Defense.notNull(element, "element");
182
183 // TODO: Tweak the ordering of operations inside the PageLoader so that this
184 // check is allowable. Currently, the component is entering active state
185 // before it loads its template.
186
187 // checkActiveLock();
188
189 // Should check the specification to see if this component
190 // allows body. Curently, this is checked by the component
191 // in render(), which is silly.
192
193 if (_body == null)
194 {
195 _body = new IRender[BODY_INIT_SIZE];
196 _body[0] = element;
197
198 _bodyCount = 1;
199 return;
200 }
201
202 // No more room? Make the array bigger.
203
204 if (_bodyCount == _body.length)
205 {
206 IRender[] newWrapped;
207
208 newWrapped = new IRender[_body.length * 2];
209
210 System.arraycopy(_body, 0, newWrapped, 0, _bodyCount);
211
212 _body = newWrapped;
213 }
214
215 _body[_bodyCount++] = element;
216 }
217
218 /**
219 * Invokes {@link #finishLoad()}. Subclasses may overide as needed, but must invoke this
220 * implementation. {@link BaseComponent} loads its HTML template.
221 */
222
223 public void finishLoad(IRequestCycle cycle, IPageLoader loader,
224 IComponentSpecification specification)
225 {
226 finishLoad();
227 }
228
229 /**
230 * Converts informal parameters into additional attributes on the curently open tag.
231 * <p>
232 * Invoked from subclasses to allow additional attributes to be specified within a tag (this
233 * works best when there is a one-to-one corespondence between an {@link IComponent}and a HTML
234 * element.
235 * <p>
236 * Iterates through the bindings for this component. Filters out bindings for formal parameters.
237 * <p>
238 * For each acceptible key, the value is extracted using {@link IBinding#getObject()}. If the
239 * value is null, no attribute is written.
240 * <p>
241 * If the value is an instance of {@link IAsset}, then {@link IAsset#buildURL()}
242 * is invoked to convert the asset to a URL.
243 * <p>
244 * Finally, {@link IMarkupWriter#attribute(String,String)}is invoked with the value (or the
245 * URL).
246 * <p>
247 * The most common use for informal parameters is to support the HTML class attribute (for use
248 * with cascading style sheets) and to specify JavaScript event handlers.
249 * <p>
250 * Components are only required to generate attributes on the result phase; this can be skipped
251 * during the rewind phase.
252 */
253
254 protected void renderInformalParameters(IMarkupWriter writer, IRequestCycle cycle)
255 {
256 String attribute;
257
258 if (_bindings == null)
259 return;
260
261 Iterator i = _bindings.entrySet().iterator();
262
263 while (i.hasNext())
264 {
265 Map.Entry entry = (Map.Entry) i.next();
266 String name = (String) entry.getKey();
267
268 if (isFormalParameter(name))
269 continue;
270
271 IBinding binding = (IBinding) entry.getValue();
272
273 Object value = binding.getObject();
274 if (value == null)
275 continue;
276
277 if (value instanceof IAsset)
278 {
279 IAsset asset = (IAsset) value;
280
281 // Get the URL of the asset and insert that.
282
283 attribute = asset.buildURL();
284 }
285 else
286 attribute = value.toString();
287
288 writer.attribute(name, attribute);
289 }
290
291 }
292
293 /** @since 4.0 */
294 private boolean isFormalParameter(String name)
295 {
296 Defense.notNull(name, "name");
297
298 return getSpecification().getParameter(name) != null;
299 }
300
301 /**
302 * Returns the named binding, or null if it doesn't exist.
303 * <p>
304 * In Tapestry 3.0, it was possible to force a binding to be stored in a component property by
305 * defining a concrete or abstract property named "nameBinding" of type {@link IBinding}. This
306 * has been removed in release 4.0 and bindings are always stored inside a Map of the component.
307 *
308 * @see #setBinding(String,IBinding)
309 */
310
311 public IBinding getBinding(String name)
312 {
313 Defense.notNull(name, "name");
314
315 if (_bindings == null)
316 return null;
317
318 return (IBinding) _bindings.get(name);
319 }
320
321 /**
322 * Returns true if the specified parameter is bound.
323 *
324 * @since 4.0
325 */
326
327 public boolean isParameterBound(String parameterName)
328 {
329 Defense.notNull(parameterName, "parameterName");
330
331 return _bindings != null && _bindings.containsKey(parameterName);
332 }
333
334 public IComponent getComponent(String id)
335 {
336 Defense.notNull(id, "id");
337
338 IComponent result = null;
339
340 if (_components != null)
341 result = (IComponent) _components.get(id);
342
343 if (result == null)
344 throw new ApplicationRuntimeException(Tapestry.format("no-such-component", this, id),
345 this, null, null);
346
347 return result;
348 }
349
350 public IComponent getContainer()
351 {
352 return _container;
353 }
354
355 public void setContainer(IComponent value)
356 {
357 checkActiveLock();
358
359 if (_container != null)
360 throw new ApplicationRuntimeException(Tapestry
361 .getMessage("AbstractComponent.attempt-to-change-container"));
362
363 _container = value;
364 }
365
366 /**
367 * Returns the name of the page, a slash, and this component's id path. Pages are different,
368 * they override this method to simply return their page name.
369 *
370 * @see #getIdPath()
371 */
372
373 public String getExtendedId()
374 {
375 if (_page == null)
376 return null;
377
378 return _page.getPageName() + "/" + getIdPath();
379 }
380
381 public String getId()
382 {
383 return _id;
384 }
385
386 public void setId(String value)
387 {
388 if (_id != null)
389 throw new ApplicationRuntimeException(Tapestry
390 .getMessage("AbstractComponent.attempt-to-change-component-id"));
391
392 _id = value;
393 }
394
395 public String getIdPath()
396 {
397 String containerIdPath;
398
399 if (_container == null)
400 throw new NullPointerException(Tapestry
401 .format("AbstractComponent.null-container", this));
402
403 containerIdPath = _container.getIdPath();
404
405 if (containerIdPath == null)
406 _idPath = _id;
407 else
408 _idPath = containerIdPath + "." + _id;
409
410 return _idPath;
411 }
412
413 public IPage getPage()
414 {
415 return _page;
416 }
417
418 public void setPage(IPage value)
419 {
420 if (_page != null)
421 throw new ApplicationRuntimeException(Tapestry
422 .getMessage("AbstractComponent.attempt-to-change-page"));
423
424 _page = value;
425 }
426
427 /**
428 * Renders all elements wrapped by the receiver.
429 */
430
431 public void renderBody(IMarkupWriter writer, IRequestCycle cycle)
432 {
433 for (int i = 0; i < _bodyCount; i++)
434 _body[i].render(writer, cycle);
435 }
436
437 /**
438 * Adds the binding with the given name, replacing any existing binding with that name.
439 * <p>
440 *
441 * @see #getBinding(String)
442 */
443
444 public void setBinding(String name, IBinding binding)
445 {
446 Defense.notNull(name, "name");
447 Defense.notNull(binding, "binding");
448
449 if (_bindings == null)
450 _bindings = new HashMap(MAP_SIZE);
451
452 _bindings.put(name, binding);
453 }
454
455 public String toString()
456 {
457 StringBuffer buffer;
458
459 buffer = new StringBuffer(super.toString());
460
461 buffer.append('[');
462
463 buffer.append(getExtendedId());
464
465 buffer.append(']');
466
467 return buffer.toString();
468 }
469
470 /**
471 * Returns an unmodifiable {@link Map}of components, keyed on component id. Never returns null,
472 * but may return an empty map. The returned map is immutable.
473 */
474
475 public Map getComponents()
476 {
477 if (_components == null)
478 return EMPTY_MAP;
479
480 return Collections.unmodifiableMap(_components);
481
482 }
483
484 public Map getAssets()
485 {
486 if (_assets == null)
487 return EMPTY_MAP;
488
489 return Collections.unmodifiableMap(_assets);
490 }
491
492 public IAsset getAsset(String name)
493 {
494 if (_assets == null)
495 return null;
496
497 return (IAsset) _assets.get(name);
498 }
499
500 public Collection getBindingNames()
501 {
502 // If no conainer, i.e. a page, then no bindings.
503
504 if (_container == null)
505 return null;
506
507 HashSet result = new HashSet();
508
509 // All the informal bindings go into the bindings Map.
510
511 if (_bindings != null)
512 result.addAll(_bindings.keySet());
513
514 // Now, iterate over the formal parameters and add the formal parameters
515 // that have a binding.
516
517 List names = getSpecification().getParameterNames();
518
519 int count = names.size();
520
521 for (int i = 0; i < count; i++)
522 {
523 String name = (String) names.get(i);
524
525 if (result.contains(name))
526 continue;
527
528 if (getBinding(name) != null)
529 result.add(name);
530 }
531
532 return result;
533 }
534
535 /**
536 * Returns an unmodifiable {@link Map}of all bindings for this component.
537 *
538 * @since 1.0.5
539 */
540
541 public Map getBindings()
542 {
543 if (_bindings == null)
544 return Collections.EMPTY_MAP;
545
546 return Collections.unmodifiableMap(_bindings);
547 }
548
549 /**
550 * Returns a {@link ListenerMap} for the component. A ListenerMap contains a number of
551 * synthetic read-only properties that implement the {@link IActionListener}interface, but in
552 * fact, cause public instance methods to be invoked.
553 *
554 * @since 1.0.2
555 */
556
557 public ListenerMap getListeners()
558 {
559 // This is what's called a violation of the Law of Demeter!
560 // This should probably be converted over to some kind of injection, as with
561 // getMessages(), etc.
562
563 if (_listeners == null)
564 _listeners = getPage().getEngine().getInfrastructure().getListenerMapSource()
565 .getListenerMapForObject(this);
566
567 return _listeners;
568 }
569
570 /**
571 * Returns the {@link IBeanProvider}for this component. This is lazily created the first time
572 * it is needed.
573 *
574 * @since 1.0.4
575 */
576
577 public IBeanProvider getBeans()
578 {
579 if (_beans == null)
580 _beans = new BeanProvider(this);
581
582 return _beans;
583 }
584
585 /**
586 * Invoked, as a convienience, from
587 * {@link #finishLoad(IRequestCycle, IPageLoader, IComponentSpecification)}. This implemenation
588 * does nothing. Subclasses may override without invoking this implementation.
589 *
590 * @since 1.0.5
591 */
592
593 protected void finishLoad()
594 {
595 }
596
597 /**
598 * The main method used to render the component. Invokes
599 * {@link #prepareForRender(IRequestCycle)}, then
600 * {@link #renderComponent(IMarkupWriter, IRequestCycle)}.
601 * {@link #cleanupAfterRender(IRequestCycle)}is invoked in a <code>finally</code> block.
602 * <p>
603 * Subclasses should not override this method; instead they will implement
604 * {@link #renderComponent(IMarkupWriter, IRequestCycle)}.
605 *
606 * @since 2.0.3
607 */
608
609 public final void render(IMarkupWriter writer, IRequestCycle cycle)
610 {
611 try
612 {
613 _rendering = true;
614
615 prepareForRender(cycle);
616
617 renderComponent(writer, cycle);
618 }
619 finally
620 {
621 _rendering = false;
622
623 cleanupAfterRender(cycle);
624 }
625 }
626
627 /**
628 * Invoked by {@link #render(IMarkupWriter, IRequestCycle)}to prepare the component to render.
629 * This implementation sets JavaBeans properties from matching bound parameters. This
630 * implementation does nothing.
631 *
632 * @since 2.0.3
633 */
634
635 protected void prepareForRender(IRequestCycle cycle)
636 {
637 }
638
639 /**
640 * Invoked by {@link #render(IMarkupWriter, IRequestCycle)}to actually render the component
641 * (with any parameter values already set). This is the method that subclasses must implement.
642 *
643 * @since 2.0.3
644 */
645
646 protected abstract void renderComponent(IMarkupWriter writer, IRequestCycle cycle);
647
648 /**
649 * Invoked by {@link #render(IMarkupWriter, IRequestCycle)}after the component renders. This
650 * implementation does nothing.
651 *
652 * @since 2.0.3
653 */
654
655 protected void cleanupAfterRender(IRequestCycle cycle)
656 {
657 }
658
659 public INamespace getNamespace()
660 {
661 return _namespace;
662 }
663
664 public void setNamespace(INamespace namespace)
665 {
666 _namespace = namespace;
667 }
668
669 /**
670 * Returns the body of the component, the element (which may be static HTML or components) that
671 * the component immediately wraps. May return null. Do not modify the returned array. The array
672 * may be padded with nulls.
673 *
674 * @since 2.3
675 * @see #getBodyCount()
676 */
677
678 public IRender[] getBody()
679 {
680 return _body;
681 }
682
683 /**
684 * Returns the active number of elements in the the body, which may be zero.
685 *
686 * @since 2.3
687 * @see #getBody()
688 */
689
690 public int getBodyCount()
691 {
692 return _bodyCount;
693 }
694
695 /**
696 * Empty implementation of
697 * {@link org.apache.tapestry.event.PageRenderListener#pageEndRender(PageEvent)}. This allows
698 * classes to implement {@link org.apache.tapestry.event.PageRenderListener}and only implement
699 * the {@link org.apache.tapestry.event.PageRenderListener#pageBeginRender(PageEvent)}method.
700 *
701 * @since 3.0
702 */
703
704 public void pageEndRender(PageEvent event)
705 {
706 }
707
708 /**
709 * Sets a property of a component.
710 *
711 * @see IComponent
712 * @since 3.0
713 * @deprecated
714 */
715 public void setProperty(String propertyName, Object value)
716 {
717 PropertyUtils.write(this, propertyName, value);
718 }
719
720 /**
721 * Gets a property of a component.
722 *
723 * @see IComponent
724 * @since 3.0
725 * @deprecated
726 */
727 public Object getProperty(String propertyName)
728 {
729 return PropertyUtils.read(this, propertyName);
730 }
731
732 /**
733 * @since 4.0
734 */
735
736 public final boolean isRendering()
737 {
738 return _rendering;
739 }
740
741 /**
742 * Returns true if the component has been transitioned into its active state by invoking
743 * {@link #enterActiveState()}
744 *
745 * @since 4.0
746 */
747
748 protected final boolean isInActiveState()
749 {
750 return _active;
751 }
752
753 /** @since 4.0 */
754 public final void enterActiveState()
755 {
756 _active = true;
757 }
758
759 /** @since 4.0 */
760
761 protected final void checkActiveLock()
762 {
763 if (_active)
764 throw new UnsupportedOperationException(TapestryMessages.componentIsLocked(this));
765 }
766
767 public Messages getMessages()
768 {
769 throw new IllegalStateException(TapestryMessages.providedByEnhancement("getMessages"));
770 }
771
772 public IComponentSpecification getSpecification()
773 {
774 throw new IllegalStateException(TapestryMessages.providedByEnhancement("getSpecification"));
775 }
776
777 /**
778 * Returns a localized message.
779 *
780 * @since 3.0
781 * @deprecated To be removed in 4.1. Use {@link #getMessages()} instead.
782 */
783
784 public String getMessage(String key)
785 {
786 return getMessages().getMessage(key);
787 }
788
789 /**
790 * Formats a localized message string, using
791 * {@link Messages#format(java.lang.String, java.lang.Object[])}.
792 *
793 * @param key
794 * the key used to obtain a localized pattern
795 * @param arguments
796 * passed to the formatter
797 * @since 3.0
798 * @deprecated To be removed in 4.1. Use {@link #getMessages()} instead.
799 */
800
801 public String format(String key, Object[] arguments)
802 {
803 return getMessages().format(key, arguments);
804 }
805
806 /**
807 * Convienience method for invoking {@link IMessages#format(String, Locale, Object)}
808 *
809 * @since 3.0
810 * @deprecated To be removed in 4.1. Use {@link #getMessages()} instead.
811 */
812
813 public String format(String key, Object argument)
814 {
815 return getMessages().format(key, argument);
816 }
817
818 /**
819 * Convienience method for invoking {@link Messages#format(String, Object, Object)}.
820 *
821 * @since 3.0
822 * @deprecated To be removed in 4.1. Use {@link #getMessages()} instead.
823 */
824
825 public String format(String key, Object argument1, Object argument2)
826 {
827 return getMessages().format(key, argument1, argument2);
828 }
829
830 /**
831 * Convienience method for {@link Messages#format(String, Object, Object, Object)}.
832 *
833 * @since 3.0
834 * @deprecated To be removed in 4.1. Use {@link #getMessages()} instead.
835 */
836
837 public String format(String key, Object argument1, Object argument2, Object argument3)
838 {
839 return getMessages().format(key, argument1, argument2, argument3);
840 }
841
842 /** @since 4.0 */
843 public final IContainedComponent getContainedComponent()
844 {
845 return _containedComponent;
846 }
847
848 /** @since 4.0 */
849 public final void setContainedComponent(IContainedComponent containedComponent)
850 {
851 Defense.notNull(containedComponent, "containedComponent");
852
853 if (_containedComponent != null)
854 throw new ApplicationRuntimeException(TapestryMessages
855 .attemptToChangeContainedComponent(this));
856
857 _containedComponent = containedComponent;
858 }
859
860 }