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