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