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.spec;
016
017 import org.apache.hivemind.ApplicationRuntimeException;
018 import org.apache.hivemind.HiveMind;
019 import org.apache.hivemind.Resource;
020 import org.apache.hivemind.util.Defense;
021 import org.apache.hivemind.util.ToStringBuilder;
022 import org.apache.tapestry.IComponent;
023 import org.apache.tapestry.IForm;
024 import org.apache.tapestry.event.BrowserEvent;
025 import org.apache.tapestry.internal.event.ComponentEventProperty;
026 import org.apache.tapestry.internal.event.EventBoundListener;
027
028 import java.util.*;
029
030 /**
031 * A specification for a component, as read from an XML specification file.
032 * <p>
033 * A specification consists of
034 * <ul>
035 * <li>An implementing class
036 * <li>An optional description
037 * <li>A set of contained components
038 * <li>Bindings for the properties of each contained component
039 * <li>A set of named assets
040 * <li>Definitions for managed beans
041 * <li>Any reserved names (used for HTML attributes)
042 * <li>Declared properties
043 * <li>Property injections
044 * </ul>
045 * <p>
046 * From this information, an actual component may be instantiated and initialized. Instantiating a
047 * component is usually a recursive process, since to initialize a container component, it is
048 * necessary to instantiate and initialize its contained components as well.
049 *
050 * @see org.apache.tapestry.IComponent
051 * @see IContainedComponent
052 * @see org.apache.tapestry.engine.IPageLoader
053 * @author Howard Lewis Ship
054 */
055
056 public class ComponentSpecification extends LocatablePropertyHolder implements IComponentSpecification
057 {
058 /**
059 * Keyed on component id, value is {@link IContainedComponent}.
060 */
061
062 protected Map _components;
063
064 /**
065 * Keyed on asset name, value is {@link IAssetSpecification}.
066 */
067
068 protected Map _assets;
069
070 /**
071 * Defines all formal parameters. Keyed on parameter name, value is
072 * {@link IParameterSpecification}.
073 */
074
075 protected Map _parameters;
076
077 /**
078 * Defines all helper beans. Keyed on name, value is {@link IBeanSpecification}.
079 *
080 * @since 1.0.4
081 */
082
083 protected Map _beans;
084
085 /**
086 * The names of all reserved informal parameter names (as lower-case). This allows the page
087 * loader to filter out any informal parameters during page load, rather than during render.
088 *
089 * @since 1.0.5
090 */
091
092 protected Set _reservedParameterNames;
093
094 private String _componentClassName;
095
096 /** @since 1.0.9 * */
097
098 private String _description;
099
100 /**
101 * Is the component allowed to have a body (that is, wrap other elements?).
102 */
103
104 private boolean _allowBody = true;
105
106 /**
107 * Is the component allow to have informal parameter specified.
108 */
109
110 private boolean _allowInformalParameters = true;
111
112 /**
113 * The XML Public Id used when the page or component specification was read (if applicable).
114 *
115 * @since 2.2
116 */
117
118 private String _publicId;
119
120 /**
121 * Indicates that the specification is for a page, not a component.
122 *
123 * @since 2.2
124 */
125
126 private boolean _pageSpecification;
127
128 /**
129 * The location from which the specification was obtained.
130 *
131 * @since 3.0
132 */
133
134 private Resource _specificationLocation;
135
136 /**
137 * A Map of {@link IPropertySpecification}keyed on the name of the property.
138 *
139 * @since 3.0
140 */
141
142 private Map _propertySpecifications;
143
144 /**
145 * List of {@link InjectSpecification}.
146 *
147 * @since 4.0
148 */
149
150 private List _injectSpecifications;
151
152 /**
153 * Keyed on property name, value is some other object (such as an IAssetSpecification) that has
154 * claimed a property of the page.
155 *
156 * @since 4.0
157 */
158
159 private Map _claimedProperties;
160
161 /**
162 * @since 4.0
163 */
164
165 private boolean _deprecated = false;
166
167 private Map _componentEvents = new HashMap();
168 private Map _elementEvents = new HashMap();
169
170 /**
171 * @throws ApplicationRuntimeException
172 * if the name already exists.
173 */
174
175 public void addAsset(String name, IAssetSpecification asset)
176 {
177 if (_assets == null)
178 _assets = new HashMap();
179
180 IAssetSpecification existing = (IAssetSpecification) _assets.get(name);
181
182 if (existing != null)
183 throw new ApplicationRuntimeException(SpecMessages.duplicateAsset(name, existing),
184 asset.getLocation(), null);
185
186 claimProperty(asset.getPropertyName(), asset);
187
188 _assets.put(name, asset);
189 }
190
191 /**
192 * @throws ApplicationRuntimeException if the id is already defined.
193 */
194
195 public void addComponent(String id, IContainedComponent component)
196 {
197 if (_components == null)
198 _components = new HashMap();
199
200 IContainedComponent existing = (IContainedComponent) _components.get(id);
201
202 if (existing != null)
203 throw new ApplicationRuntimeException(SpecMessages.duplicateComponent(id, existing),
204 component.getLocation(), null);
205
206 _components.put(id, component);
207
208 claimProperty(component.getPropertyName(), component);
209 }
210
211 /**
212 * Adds the parameter. The name is added as a reserved name.
213 *
214 * @throws ApplicationRuntimeException if the name already exists.
215 */
216
217 public void addParameter(IParameterSpecification spec)
218 {
219 if (_parameters == null)
220 _parameters = new HashMap();
221
222 String name = spec.getParameterName();
223
224 addParameterByName(name, spec);
225
226 Iterator i = spec.getAliasNames().iterator();
227 while (i.hasNext())
228 {
229 String alias = (String) i.next();
230
231 addParameterByName(alias, spec);
232 }
233
234 claimProperty(spec.getPropertyName(), spec);
235 }
236
237 private void addParameterByName(String name, IParameterSpecification spec)
238 {
239 IParameterSpecification existing = (IParameterSpecification) _parameters.get(name);
240
241 if (existing != null)
242 throw new ApplicationRuntimeException(SpecMessages.duplicateParameter(name, existing),
243 spec.getLocation(), null);
244
245 _parameters.put(name, spec);
246
247 addReservedParameterName(name);
248 }
249
250 /**
251 * Returns true if the component is allowed to wrap other elements (static HTML or other
252 * components). The default is true.
253 *
254 * @see #setAllowBody(boolean)
255 */
256
257 public boolean getAllowBody()
258 {
259 return _allowBody;
260 }
261
262 /**
263 * Returns true if the component allows informal parameters (parameters not formally defined).
264 * Informal parameters are generally used to create additional HTML attributes for an HTML tag
265 * rendered by the component. This is often used to specify JavaScript event handlers or the
266 * class of the component (for Cascarding Style Sheets).
267 * <p>
268 * The default value is true.
269 *
270 * @see #setAllowInformalParameters(boolean)
271 */
272
273 public boolean getAllowInformalParameters()
274 {
275 return _allowInformalParameters;
276 }
277
278 /**
279 * Returns the {@link IAssetSpecification}with the given name, or null if no such specification
280 * exists.
281 *
282 * @see #addAsset(String,IAssetSpecification)
283 */
284
285 public IAssetSpecification getAsset(String name)
286 {
287 return (IAssetSpecification) get(_assets, name);
288 }
289
290 /**
291 * Returns a <code>List</code> of the String names of all assets, in alphabetical order.
292 */
293
294 public List getAssetNames()
295 {
296 return sortedKeys(_assets);
297 }
298
299 /**
300 * Returns the specification of a contained component with the given id, or null if no such
301 * contained component exists.
302 *
303 * @see #addComponent(String, IContainedComponent)
304 */
305
306 public IContainedComponent getComponent(String id)
307 {
308 return (IContainedComponent) get(_components, id);
309 }
310
311
312 public String getComponentClassName()
313 {
314 return _componentClassName;
315 }
316
317 /**
318 * Returns an <code>List</code> of the String names of the {@link IContainedComponent}s for
319 * this component.
320 *
321 * @see #addComponent(String, IContainedComponent)
322 */
323
324 public List getComponentIds()
325 {
326 return sortedKeys(_components);
327 }
328
329 /**
330 * Returns the specification of a parameter with the given name, or null if no such parameter
331 * exists.
332 *
333 * @see #addParameterByName(String, IParameterSpecification)
334 */
335
336 public IParameterSpecification getParameter(String name)
337 {
338 return (IParameterSpecification) get(_parameters, name);
339 }
340
341 public Collection getRequiredParameters()
342 {
343 if (_parameters == null)
344 return Collections.EMPTY_LIST;
345
346 Collection result = new ArrayList();
347
348 Iterator i = _parameters.entrySet().iterator();
349 while (i.hasNext())
350 {
351 Map.Entry entry = (Map.Entry) i.next();
352 String name = (String) entry.getKey();
353 IParameterSpecification spec = (IParameterSpecification) entry.getValue();
354
355 if (!spec.isRequired())
356 continue;
357
358 if (!name.equals(spec.getParameterName()))
359 continue;
360
361 result.add(spec);
362 }
363
364 return result;
365 }
366
367 /**
368 * Returns a List of of String names of all parameters. This list is in alphabetical order.
369 *
370 * @see #addParameterByName(String, IParameterSpecification)
371 */
372
373 public List getParameterNames()
374 {
375 return sortedKeys(_parameters);
376 }
377
378 public void setAllowBody(boolean value)
379 {
380 _allowBody = value;
381 }
382
383 public void setAllowInformalParameters(boolean value)
384 {
385 _allowInformalParameters = value;
386 }
387
388 public void setComponentClassName(String value)
389 {
390 _componentClassName = value;
391 }
392
393 /**
394 * @since 1.0.4
395 * @throws ApplicationRuntimeException
396 * if the bean already has a specification.
397 */
398
399 public void addBeanSpecification(String name, IBeanSpecification specification)
400 {
401 if (_beans == null)
402 _beans = new HashMap();
403
404 IBeanSpecification existing = (IBeanSpecification) _beans.get(name);
405
406 if (existing != null)
407 throw new ApplicationRuntimeException(SpecMessages.duplicateBean(name, existing),
408 specification.getLocation(), null);
409
410 claimProperty(specification.getPropertyName(), specification);
411
412 _beans.put(name, specification);
413 }
414
415 /**
416 * Returns the {@link IBeanSpecification}for the given name, or null if not such specification
417 * exists.
418 *
419 * @since 1.0.4
420 */
421
422 public IBeanSpecification getBeanSpecification(String name)
423 {
424 return (IBeanSpecification) get(_beans, name);
425 }
426
427 /**
428 * Returns an unmodifiable collection of the names of all beans.
429 */
430
431 public Collection getBeanNames()
432 {
433 if (_beans == null)
434 return Collections.EMPTY_LIST;
435
436 return Collections.unmodifiableCollection(_beans.keySet());
437 }
438
439 /**
440 * Adds the value as a reserved name. Reserved names are not allowed as the names of informal
441 * parameters. Since the comparison is caseless, the value is converted to lowercase before
442 * being stored.
443 *
444 * @since 1.0.5
445 */
446
447 public void addReservedParameterName(String value)
448 {
449 if (_reservedParameterNames == null)
450 _reservedParameterNames = new HashSet();
451
452 _reservedParameterNames.add(value.toLowerCase());
453 }
454
455 /**
456 * Returns true if the value specified is in the reserved name list. The comparison is caseless.
457 * All formal parameters are automatically in the reserved name list, as well as any additional
458 * reserved names specified in the component specification. The latter refer to HTML attributes
459 * generated directly by the component.
460 *
461 * @since 1.0.5
462 */
463
464 public boolean isReservedParameterName(String value)
465 {
466 if (_reservedParameterNames == null)
467 return false;
468
469 return _reservedParameterNames.contains(value.toLowerCase());
470 }
471
472 public String toString()
473 {
474 ToStringBuilder builder = new ToStringBuilder(this);
475
476 builder.append("componentClassName", _componentClassName);
477 builder.append("pageSpecification", _pageSpecification);
478 builder.append("specificationLocation", _specificationLocation);
479 builder.append("allowBody", _allowBody);
480 builder.append("allowInformalParameter", _allowInformalParameters);
481
482 return builder.toString();
483 }
484
485 /**
486 * Returns the documentation for this component.
487 *
488 * @since 1.0.9
489 */
490
491 public String getDescription()
492 {
493 return _description;
494 }
495
496 /**
497 * Sets the documentation for this component.
498 *
499 * @since 1.0.9
500 */
501
502 public void setDescription(String description)
503 {
504 _description = description;
505 }
506
507 /**
508 * Returns the XML Public Id for the specification file, or null if not applicable.
509 * <p>
510 * This method exists as a convienience for the Spindle plugin. A previous method used an
511 * arbitrary version string, the public id is more useful and less ambiguous.
512 *
513 * @since 2.2
514 */
515
516 public String getPublicId()
517 {
518 return _publicId;
519 }
520
521 /** @since 2.2 * */
522
523 public void setPublicId(String publicId)
524 {
525 _publicId = publicId;
526 }
527
528 /**
529 * Returns true if the specification is known to be a page specification and not a component
530 * specification. Earlier versions of the framework did not distinguish between the two, but
531 * starting in 2.2, there are seperate XML entities for pages and components. Pages omit several
532 * attributes and entities related to parameters, as parameters only make sense for components.
533 *
534 * @since 2.2
535 */
536
537 public boolean isPageSpecification()
538 {
539 return _pageSpecification;
540 }
541
542 /** @since 2.2 * */
543
544 public void setPageSpecification(boolean pageSpecification)
545 {
546 _pageSpecification = pageSpecification;
547 }
548
549 /** @since 2.2 * */
550
551 private List sortedKeys(Map input)
552 {
553 if (input == null)
554 return Collections.EMPTY_LIST;
555
556 List result = new ArrayList(input.keySet());
557
558 Collections.sort(result);
559
560 return result;
561 }
562
563 /** @since 2.2 * */
564
565 private Object get(Map map, Object key)
566 {
567 if (map == null)
568 return null;
569
570 return map.get(key);
571 }
572
573 /** @since 3.0 * */
574
575 public Resource getSpecificationLocation()
576 {
577 return _specificationLocation;
578 }
579
580 /** @since 3.0 * */
581
582 public void setSpecificationLocation(Resource specificationLocation)
583 {
584 _specificationLocation = specificationLocation;
585 }
586
587 /**
588 * Adds a new property specification. The name of the property must not already be defined (and
589 * must not change after being added).
590 *
591 * @since 3.0
592 */
593
594 public void addPropertySpecification(IPropertySpecification spec)
595 {
596 if (_propertySpecifications == null)
597 _propertySpecifications = new HashMap();
598
599 String name = spec.getName();
600 IPropertySpecification existing = (IPropertySpecification) _propertySpecifications
601 .get(name);
602
603 if (existing != null)
604 throw new ApplicationRuntimeException(SpecMessages.duplicateProperty(name, existing), spec.getLocation(), null);
605
606 claimProperty(name, spec);
607
608 _propertySpecifications.put(name, spec);
609 }
610
611 /**
612 * Returns a sorted, immutable list of the names of all
613 * {@link org.apache.tapestry.spec.IPropertySpecification}s.
614 *
615 * @since 3.0
616 */
617
618 public List getPropertySpecificationNames()
619 {
620 return sortedKeys(_propertySpecifications);
621 }
622
623 /**
624 * Returns the named {@link org.apache.tapestry.spec.IPropertySpecification}, or null if no
625 * such specification exist.
626 *
627 * @since 3.0
628 * @see #addPropertySpecification(IPropertySpecification)
629 */
630
631 public IPropertySpecification getPropertySpecification(String name)
632 {
633 return (IPropertySpecification) get(_propertySpecifications, name);
634 }
635
636 public void addInjectSpecification(InjectSpecification spec)
637 {
638 if (_injectSpecifications == null)
639 _injectSpecifications = new ArrayList();
640
641 claimProperty(spec.getProperty(), spec);
642
643 _injectSpecifications.add(spec);
644 }
645
646 public List getInjectSpecifications()
647 {
648 return safeList(_injectSpecifications);
649 }
650
651 private List safeList(List input)
652 {
653 if (input == null)
654 return Collections.EMPTY_LIST;
655
656 return Collections.unmodifiableList(input);
657 }
658
659 private void claimProperty(String propertyName, Object subSpecification)
660 {
661 if (propertyName == null)
662 return;
663
664 if (_claimedProperties == null)
665 _claimedProperties = new HashMap();
666
667 Object existing = _claimedProperties.get(propertyName);
668
669 if (existing != null)
670 throw new ApplicationRuntimeException(SpecMessages.claimedProperty(
671 propertyName,
672 existing), HiveMind.getLocation(subSpecification), null);
673
674 _claimedProperties.put(propertyName, subSpecification);
675 }
676
677 /** @since 4.0 */
678 public boolean isDeprecated()
679 {
680 return _deprecated;
681 }
682
683 /** @since 4.0 */
684 public void setDeprecated(boolean deprecated)
685 {
686 _deprecated = deprecated;
687 }
688
689 public Set getReservedParameterNames()
690 {
691 if (_reservedParameterNames == null)
692 return Collections.EMPTY_SET;
693
694 return Collections.unmodifiableSet(_reservedParameterNames);
695 }
696
697 /**
698 * {@inheritDoc}
699 */
700 public void addEventListener(String componentId, String[] events,
701 String methodName, String formId, boolean validateForm, boolean async, boolean focus, boolean autoSubmit)
702 {
703 ComponentEventProperty property = getComponentEvents(componentId);
704 if (property == null) {
705 property = new ComponentEventProperty(componentId);
706 _componentEvents.put(componentId, property);
707 }
708
709 property.addListener(events, methodName, formId, validateForm, async, focus, autoSubmit);
710 }
711
712 /**
713 * {@inheritDoc}
714 */
715 public void addElementEventListener(String elementId, String[] events,
716 String methodName, String formId, boolean validateForm, boolean async, boolean focus)
717 {
718 ComponentEventProperty property = getElementEvents(elementId);
719 if (property == null) {
720 property = new ComponentEventProperty(elementId);
721 _elementEvents.put(elementId, property);
722 }
723
724 property.addListener(events, methodName, formId, validateForm, async, focus, true);
725 }
726
727 public void connectAutoSubmitEvents(IComponent component, IForm form)
728 {
729 Defense.notNull(form, "form");
730
731 ComponentEventProperty property = getComponentEvents(component.getExtendedId());
732
733 if (property == null)
734 return;
735
736 property.connectAutoSubmitEvents(form.getExtendedId());
737 }
738
739 public void rewireComponentId(String componentId, String extendedId, String idPath)
740 {
741 ComponentEventProperty prop = getComponentEvents(componentId);
742 if (prop == null)
743 return;
744
745 if (_componentEvents.containsKey(extendedId))
746 return;
747
748 try {
749
750 ComponentEventProperty clone = (ComponentEventProperty) prop.clone();
751
752 clone.rewireComponentId(extendedId, idPath);
753
754 _componentEvents.put(extendedId, clone);
755
756 } catch (CloneNotSupportedException e) {
757
758 throw new ApplicationRuntimeException(e);
759 }
760 }
761
762 /**
763 * {@inheritDoc}
764 */
765 public ComponentEventProperty getComponentEvents(String id)
766 {
767 return (ComponentEventProperty)_componentEvents.get(id);
768 }
769
770 public Map getComponentEvents()
771 {
772 return _componentEvents;
773 }
774
775 /**
776 * {@inheritDoc}
777 */
778 public ComponentEventProperty getElementEvents(String id)
779 {
780 return (ComponentEventProperty)_elementEvents.get(id);
781 }
782
783 /**
784 * {@inheritDoc}
785 */
786 public Map getElementEvents()
787 {
788 return _elementEvents;
789 }
790
791 /**
792 * {@inheritDoc}
793 */
794 public EventBoundListener[] getFormEvents(String formId, BrowserEvent event)
795 {
796 List ret = new ArrayList();
797
798 Iterator it = _componentEvents.keySet().iterator();
799 while (it.hasNext()) {
800
801 String compId = (String)it.next();
802 ComponentEventProperty prop = (ComponentEventProperty)_componentEvents.get(compId);
803
804 ret.addAll(prop.getFormEventListeners(formId, event, null));
805 }
806
807 it = _elementEvents.keySet().iterator();
808 while (it.hasNext()) {
809
810 String compId = (String)it.next();
811 ComponentEventProperty prop = (ComponentEventProperty)_elementEvents.get(compId);
812
813 ret.addAll(prop.getFormEventListeners(formId, event, null));
814 }
815
816 return (EventBoundListener[])ret.toArray(new EventBoundListener[ret.size()]);
817 }
818
819 /**
820 * {@inheritDoc}
821 */
822 public boolean hasElementEvents()
823 {
824 return _elementEvents.size() > 0;
825 }
826
827 /**
828 * {@inheritDoc}
829 */
830 public int hashCode()
831 {
832 final int prime = 31;
833 int result = 1;
834 result = prime * result + ((_componentClassName == null) ? 0 : _componentClassName.hashCode());
835 result = prime * result + ((_description == null) ? 0 : _description.hashCode());
836 result = prime * result + (_pageSpecification ? 1231 : 1237);
837 result = prime * result + ((_publicId == null) ? 0 : _publicId.hashCode());
838 return result;
839 }
840
841 /**
842 * {@inheritDoc}
843 */
844 public boolean equals(Object obj)
845 {
846 if (this == obj) return true;
847 if (obj == null) return false;
848 if (getClass() != obj.getClass()) return false;
849 final ComponentSpecification other = (ComponentSpecification) obj;
850 if (_componentClassName == null) {
851 if (other._componentClassName != null) return false;
852 } else if (!_componentClassName.equals(other._componentClassName)) return false;
853 if (_description == null) {
854 if (other._description != null) return false;
855 } else if (!_description.equals(other._description)) return false;
856 if (_pageSpecification != other._pageSpecification) return false;
857 if (_publicId == null) {
858 if (other._publicId != null) return false;
859 } else if (!_publicId.equals(other._publicId)) return false;
860 if (_specificationLocation == null) {
861 if (other._specificationLocation != null) return false;
862 } else if (!_specificationLocation.getPath().equals(other._specificationLocation.getPath())) return false;
863 return true;
864 }
865
866
867 }