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.tapestry.bean.BeanProvider;
030 import org.apache.tapestry.engine.IPageLoader;
031 import org.apache.tapestry.event.BrowserEvent;
032 import org.apache.tapestry.event.PageEvent;
033 import org.apache.tapestry.listener.ListenerMap;
034 import org.apache.tapestry.services.ComponentRenderWorker;
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 /** @since 4.1 */
058 protected String _clientId;
059
060 /**
061 * The page that contains the component, possibly itself (if the component is in fact, a page).
062 */
063
064 private IPage _page;
065
066 /**
067 * The component which contains the component. This will only be null if the component is
068 * actually a page.
069 */
070
071 private IComponent _container;
072
073 /**
074 * The simple id of this component.
075 */
076
077 private String _id;
078
079 /**
080 * The fully qualified id of this component. This is calculated the first time it is needed,
081 * then cached for later.
082 */
083
084 private String _idPath;
085
086 /**
087 * A {@link Map}of all bindings (for which there isn't a corresponding JavaBeans property); the
088 * keys are the names of formal and informal parameters.
089 */
090
091 private Map _bindings;
092
093 private Map _components;
094
095 private INamespace _namespace;
096
097 /**
098 * The number of {@link IRender}objects in the body of this component.
099 */
100
101 private int _bodyCount = 0;
102
103 /**
104 * An aray of elements in the body of this component.
105 */
106
107 private IRender[] _body;
108
109 /**
110 * The components' asset map.
111 */
112
113 private Map _assets;
114
115 /**
116 * A mapping that allows public instance methods to be dressed up as {@link IActionListener}
117 * listener objects.
118 *
119 * @since 1.0.2
120 */
121
122 private ListenerMap _listeners;
123
124 /**
125 * A bean provider; these are lazily created as needed.
126 *
127 * @since 1.0.4
128 */
129
130 private IBeanProvider _beans;
131
132 /**
133 * Returns true if the component is currently rendering.
134 *
135 * @see #prepareForRender(IRequestCycle)
136 * @see #cleanupAfterRender(IRequestCycle)
137 * @since 4.0
138 */
139
140 private boolean _rendering;
141
142 /**
143 * @since 4.0
144 */
145
146 private boolean _active;
147
148 /** @since 4.0 */
149
150 private IContainedComponent _containedComponent;
151
152
153 public void addAsset(String name, IAsset asset)
154 {
155 Defense.notNull(name, "name");
156 Defense.notNull(asset, "asset");
157
158 checkActiveLock();
159
160 if (_assets == null)
161 _assets = new HashMap(MAP_SIZE);
162
163 _assets.put(name, asset);
164 }
165
166 public void addComponent(IComponent component)
167 {
168 Defense.notNull(component, "component");
169
170 checkActiveLock();
171
172 if (_components == null)
173 _components = new HashMap(MAP_SIZE);
174
175 _components.put(component.getId(), component);
176 }
177
178 /**
179 * Adds an element (which may be static text or a component) as a body element of this
180 * component. Such elements are rendered by {@link #renderBody(IMarkupWriter, IRequestCycle)}.
181 *
182 * @since 2.2
183 */
184
185 public void addBody(IRender element)
186 {
187 Defense.notNull(element, "element");
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 * Overriden by {@link AbstractFormComponent} to provide different
294 * behaviour.
295 *
296 * @param writer
297 * @param cycle
298 */
299 protected void renderIdAttribute(IMarkupWriter writer, IRequestCycle cycle)
300 {
301 // try to generate a client id if needed/possible
302 if (_clientId == null) {
303 String id = getBoundId();
304
305 if (id == null) id = getId();
306
307 _clientId = cycle.getUniqueId(TapestryUtils.convertTapestryIdToNMToken(id));
308 }
309
310 String id = getClientId();
311
312 if (id != null)
313 writer.attribute("id", id);
314 }
315
316 /** @since 4.0 */
317 private boolean isFormalParameter(String name)
318 {
319 Defense.notNull(name, "name");
320
321 return getSpecification().getParameter(name) != null;
322 }
323
324 /**
325 * Returns the named binding, or null if it doesn't exist.
326 * <p>
327 * In Tapestry 3.0, it was possible to force a binding to be stored in a component property by
328 * defining a concrete or abstract property named "nameBinding" of type {@link IBinding}. This
329 * has been removed in release 4.0 and bindings are always stored inside a Map of the component.
330 *
331 * @see #setBinding(String,IBinding)
332 */
333
334 public IBinding getBinding(String name)
335 {
336 Defense.notNull(name, "name");
337
338 if (_bindings == null)
339 return null;
340
341 return (IBinding) _bindings.get(name);
342 }
343
344 /**
345 * Returns true if the specified parameter is bound.
346 *
347 * @since 4.0
348 */
349
350 public boolean isParameterBound(String parameterName)
351 {
352 Defense.notNull(parameterName, "parameterName");
353
354 return _bindings != null && _bindings.containsKey(parameterName);
355 }
356
357 public IComponent getComponent(String id)
358 {
359 Defense.notNull(id, "id");
360
361 IComponent result = null;
362
363 if (_components != null)
364 result = (IComponent) _components.get(id);
365
366 if (result == null)
367 throw new ApplicationRuntimeException(Tapestry.format("no-such-component", this, id),
368 this, null, null);
369
370 return result;
371 }
372
373 public IComponent getContainer()
374 {
375 return _container;
376 }
377
378 public void setContainer(IComponent value)
379 {
380 checkActiveLock();
381
382 if (_container != null)
383 throw new ApplicationRuntimeException(Tapestry
384 .getMessage("AbstractComponent.attempt-to-change-container"));
385
386 _container = value;
387 }
388
389 /**
390 * Returns the name of the page, a slash, and this component's id path. Pages are different,
391 * they override this method to simply return their page name.
392 *
393 * @see #getIdPath()
394 */
395
396 public String getExtendedId()
397 {
398 if (_page == null)
399 return null;
400
401 return _page.getPageName() + "/" + getIdPath();
402 }
403
404 public String getId()
405 {
406 return _id;
407 }
408
409 public void setId(String value)
410 {
411 if (_id != null)
412 throw new ApplicationRuntimeException(Tapestry
413 .getMessage("AbstractComponent.attempt-to-change-component-id"));
414
415 _id = value;
416 }
417
418 public String getIdPath()
419 {
420 String containerIdPath;
421
422 if (_container == null)
423 throw new NullPointerException(Tapestry
424 .format("AbstractComponent.null-container", this));
425
426 containerIdPath = _container.getIdPath();
427
428 if (containerIdPath == null)
429 _idPath = _id;
430 else
431 _idPath = containerIdPath + "." + _id;
432
433 return _idPath;
434 }
435
436 /**
437 *
438 * {@inheritDoc}
439 * @since 4.1
440 */
441 public String getClientId()
442 {
443 if (_clientId != null)
444 return _clientId;
445
446 String boundId = getBoundId();
447 if (boundId == null)
448 return getId();
449
450 return boundId;
451 }
452
453 protected void setClientId(String id)
454 {
455 _clientId = id;
456 }
457
458 String getBoundId()
459 {
460 if (_bindings == null)
461 return null;
462
463 IBinding id = (IBinding)_bindings.get("id");
464 if (id == null)
465 return null;
466
467 return id.getObject().toString();
468 }
469
470 public IPage getPage()
471 {
472 return _page;
473 }
474
475 public void setPage(IPage value)
476 {
477 if (_page != null)
478 throw new ApplicationRuntimeException(Tapestry
479 .getMessage("AbstractComponent.attempt-to-change-page"));
480
481 _page = value;
482 }
483
484 /**
485 * Renders all elements wrapped by the receiver.
486 */
487
488 public void renderBody(IMarkupWriter writer, IRequestCycle cycle)
489 {
490 for (int i = 0; i < _bodyCount; i++)
491 cycle.getResponseBuilder().render(writer, _body[i], cycle);
492 }
493
494 /**
495 * Adds the binding with the given name, replacing any existing binding with that name.
496 * <p>
497 *
498 * @see #getBinding(String)
499 */
500
501 public void setBinding(String name, IBinding binding)
502 {
503 Defense.notNull(name, "name");
504 Defense.notNull(binding, "binding");
505
506 if (_bindings == null)
507 _bindings = new HashMap(MAP_SIZE);
508
509 _bindings.put(name, binding);
510 }
511
512 public String toString()
513 {
514 StringBuffer buffer;
515
516 buffer = new StringBuffer(super.toString());
517
518 buffer.append('[');
519
520 buffer.append(getExtendedId());
521
522 buffer.append(']');
523
524 return buffer.toString();
525 }
526
527 /**
528 * Returns an unmodifiable {@link Map}of components, keyed on component id. Never returns null,
529 * but may return an empty map. The returned map is immutable.
530 */
531
532 public Map getComponents()
533 {
534 if (_components == null)
535 return EMPTY_MAP;
536
537 return Collections.unmodifiableMap(_components);
538
539 }
540
541 public Map getAssets()
542 {
543 if (_assets == null)
544 return EMPTY_MAP;
545
546 return Collections.unmodifiableMap(_assets);
547 }
548
549 public IAsset getAsset(String name)
550 {
551 if (_assets == null)
552 return null;
553
554 return (IAsset) _assets.get(name);
555 }
556
557 public Collection getBindingNames()
558 {
559 // If no conainer, i.e. a page, then no bindings.
560
561 if (_container == null)
562 return null;
563
564 HashSet result = new HashSet();
565
566 // All the informal bindings go into the bindings Map.
567
568 if (_bindings != null)
569 result.addAll(_bindings.keySet());
570
571 // Now, iterate over the formal parameters and add the formal parameters
572 // that have a binding.
573
574 List names = getSpecification().getParameterNames();
575
576 int count = names.size();
577
578 for (int i = 0; i < count; i++)
579 {
580 String name = (String) names.get(i);
581
582 if (result.contains(name))
583 continue;
584
585 if (getBinding(name) != null)
586 result.add(name);
587 }
588
589 return result;
590 }
591
592 /**
593 * Returns an unmodifiable {@link Map}of all bindings for this component.
594 *
595 * @since 1.0.5
596 */
597
598 public Map getBindings()
599 {
600 if (_bindings == null)
601 return Collections.EMPTY_MAP;
602
603 return Collections.unmodifiableMap(_bindings);
604 }
605
606 /**
607 * Returns a {@link ListenerMap} for the component. A ListenerMap contains a number of
608 * synthetic read-only properties that implement the {@link IActionListener}interface, but in
609 * fact, cause public instance methods to be invoked.
610 *
611 * @since 1.0.2
612 */
613
614 public ListenerMap getListeners()
615 {
616 // This is what's called a violation of the Law of Demeter!
617 // This should probably be converted over to some kind of injection, as with
618 // getMessages(), etc.
619
620 if (_listeners == null)
621 _listeners = getPage().getEngine().getInfrastructure().getListenerMapSource()
622 .getListenerMapForObject(this);
623
624 return _listeners;
625 }
626
627 /**
628 * Returns the {@link IBeanProvider}for this component. This is lazily created the first time
629 * it is needed.
630 *
631 * @since 1.0.4
632 */
633
634 public IBeanProvider getBeans()
635 {
636 if (_beans == null)
637 _beans = new BeanProvider(this);
638
639 return _beans;
640 }
641
642 /**
643 * Invoked, as a convienience, from
644 * {@link #finishLoad(IRequestCycle, IPageLoader, IComponentSpecification)}. This implemenation
645 * does nothing. Subclasses may override without invoking this implementation.
646 *
647 * @since 1.0.5
648 */
649
650 protected void finishLoad()
651 {
652 }
653
654 /**
655 * The main method used to render the component. Invokes
656 * {@link #prepareForRender(IRequestCycle)}, then
657 * {@link #renderComponent(IMarkupWriter, IRequestCycle)}.
658 * {@link #cleanupAfterRender(IRequestCycle)}is invoked in a <code>finally</code> block.
659 * <p>
660 * Subclasses should not override this method; instead they will implement
661 * {@link #renderComponent(IMarkupWriter, IRequestCycle)}.
662 *
663 * @since 2.0.3
664 */
665
666 public final void render(IMarkupWriter writer, IRequestCycle cycle)
667 {
668 try
669 {
670 _rendering = true;
671
672 cycle.renderStackPush(this);
673
674 prepareForRender(cycle);
675
676 renderComponent(writer, cycle);
677
678 getRenderWorker().renderComponent(cycle, this);
679
680 }
681 finally
682 {
683 _rendering = false;
684
685 cleanupAfterRender(cycle);
686
687 // @since 4.1
688 _clientId = null;
689
690 cycle.renderStackPop();
691 }
692 }
693
694 /**
695 * Invoked by {@link #render(IMarkupWriter, IRequestCycle)}to prepare the component to render.
696 * This implementation sets JavaBeans properties from matching bound parameters. The default
697 * implementation of this method is empty.
698 *
699 * @since 2.0.3
700 */
701
702 protected void prepareForRender(IRequestCycle cycle)
703 {
704 }
705
706 /**
707 * Invoked by {@link #render(IMarkupWriter, IRequestCycle)}to actually render the component
708 * (with any parameter values already set). This is the method that subclasses must implement.
709 *
710 * @since 2.0.3
711 */
712
713 protected abstract void renderComponent(IMarkupWriter writer, IRequestCycle cycle);
714
715 /**
716 * Invoked by {@link #render(IMarkupWriter, IRequestCycle)}after the component renders. This
717 * implementation does nothing.
718 *
719 * @since 2.0.3
720 */
721
722 protected void cleanupAfterRender(IRequestCycle cycle)
723 {
724 }
725
726 public INamespace getNamespace()
727 {
728 return _namespace;
729 }
730
731 public void setNamespace(INamespace namespace)
732 {
733 _namespace = namespace;
734 }
735
736 /**
737 * Returns the body of the component, the element (which may be static HTML or components) that
738 * the component immediately wraps. May return null. Do not modify the returned array. The array
739 * may be padded with nulls.
740 *
741 * @since 2.3
742 * @see #getBodyCount()
743 */
744
745 public IRender[] getBody()
746 {
747 return _body;
748 }
749
750 /**
751 * Returns the active number of elements in the the body, which may be zero.
752 *
753 * @since 2.3
754 * @see #getBody()
755 */
756
757 public int getBodyCount()
758 {
759 return _bodyCount;
760 }
761
762 /**
763 * Empty implementation of
764 * {@link org.apache.tapestry.event.PageRenderListener#pageEndRender(PageEvent)}. This allows
765 * classes to implement {@link org.apache.tapestry.event.PageRenderListener}and only implement
766 * the {@link org.apache.tapestry.event.PageRenderListener#pageBeginRender(PageEvent)}method.
767 *
768 * @since 3.0
769 */
770
771 public void pageEndRender(PageEvent event)
772 {
773 }
774
775 /**
776 * @since 4.0
777 */
778
779 public final boolean isRendering()
780 {
781 return _rendering;
782 }
783
784 /**
785 * Returns true if the component has been transitioned into its active state by invoking
786 * {@link #enterActiveState()}.
787 *
788 * @since 4.0
789 */
790
791 protected final boolean isInActiveState()
792 {
793 return _active;
794 }
795
796 /** @since 4.0 */
797 public final void enterActiveState()
798 {
799 _active = true;
800 }
801
802 /** @since 4.0 */
803
804 protected final void checkActiveLock()
805 {
806 if (_active)
807 throw new UnsupportedOperationException(TapestryMessages.componentIsLocked(this));
808 }
809
810 public Messages getMessages()
811 {
812 throw new IllegalStateException(TapestryMessages.providedByEnhancement("getMessages"));
813 }
814
815 public IComponentSpecification getSpecification()
816 {
817 throw new IllegalStateException(TapestryMessages.providedByEnhancement("getSpecification"));
818 }
819
820 /** @since 4.0 */
821 public final IContainedComponent getContainedComponent()
822 {
823 return _containedComponent;
824 }
825
826 /** @since 4.0 */
827 public final void setContainedComponent(IContainedComponent containedComponent)
828 {
829 Defense.notNull(containedComponent, "containedComponent");
830
831 if (_containedComponent != null)
832 throw new ApplicationRuntimeException(TapestryMessages
833 .attemptToChangeContainedComponent(this));
834
835 _containedComponent = containedComponent;
836 }
837
838 /**
839 * {@inheritDoc}
840 */
841 public ComponentEventInvoker getEventInvoker()
842 {
843 throw new IllegalStateException(TapestryMessages.providedByEnhancement("getEventInvoker"));
844 }
845
846 /**
847 * {@inheritDoc}
848 */
849 public void triggerEvent(IRequestCycle cycle, BrowserEvent event)
850 {
851 getEventInvoker().invokeListeners(this, cycle, event);
852 }
853
854 /**
855 * {@inheritDoc}
856 */
857 public ComponentRenderWorker getRenderWorker()
858 {
859 throw new IllegalStateException(TapestryMessages.providedByEnhancement("getRenderWorker"));
860 }
861
862 /**
863 * {@inheritDoc}
864 */
865 public boolean isStateful()
866 {
867 return false;
868 }
869 }