0001 /*---
0002    Copyright 2006-2007 Visual Systems Corporation.
0003    http://www.vscorp.com
0004 
0005    Licensed under the Apache License, Version 2.0 (the "License");
0006    you may not use this file except in compliance with the License.
0007    You may obtain a copy of the License at
0008    
0009         http://www.apache.org/licenses/LICENSE-2.0
0010    
0011    Unless required by applicable law or agreed to in writing, software
0012    distributed under the License is distributed on an "AS IS" BASIS,
0013    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
0014    See the License for the specific language governing permissions and
0015    limitations under the License.
0016 ---*/
0017 package net.sourceforge.wicketwebbeans.model;
0018 
0019 import java.beans.PropertyChangeListener;
0020 import java.beans.PropertyDescriptor;
0021 import java.io.File;
0022 import java.io.IOException;
0023 import java.io.InputStream;
0024 import java.io.Serializable;
0025 import java.lang.annotation.Annotation;
0026 import java.lang.reflect.Method;
0027 import java.net.URISyntaxException;
0028 import java.net.URL;
0029 import java.util.ArrayList;
0030 import java.util.Collections;
0031 import java.util.Comparator;
0032 import java.util.HashMap;
0033 import java.util.HashSet;
0034 import java.util.List;
0035 import java.util.Map;
0036 import java.util.Set;
0037 import java.util.logging.Logger;
0038 
0039 import net.sourceforge.wicketwebbeans.actions.BeanSubmitButton;
0040 import net.sourceforge.wicketwebbeans.annotations.Action;
0041 import net.sourceforge.wicketwebbeans.annotations.Bean;
0042 import net.sourceforge.wicketwebbeans.annotations.Beans;
0043 import net.sourceforge.wicketwebbeans.annotations.Property;
0044 import net.sourceforge.wicketwebbeans.annotations.Tab;
0045 import net.sourceforge.wicketwebbeans.containers.BeanForm;
0046 import net.sourceforge.wicketwebbeans.containers.BeanGridPanel;
0047 import net.sourceforge.wicketwebbeans.fields.EmptyField;
0048 import net.sourceforge.wicketwebbeans.fields.Field;
0049 import net.sourceforge.wicketwebbeans.model.api.JBeans;
0050 
0051 import org.apache.commons.beanutils.MethodUtils;
0052 import org.apache.commons.beanutils.PropertyUtils;
0053 import org.apache.wicket.Component;
0054 import org.apache.wicket.ajax.AjaxRequestTarget;
0055 import org.apache.wicket.behavior.AttributeAppender;
0056 import org.apache.wicket.markup.html.form.Form;
0057 import org.apache.wicket.markup.html.panel.Panel;
0058 import org.apache.wicket.model.IModel;
0059 import org.apache.wicket.model.Model;
0060 import org.apache.wicket.util.string.Strings;
0061 
0062 
0063 /**
0064  * Represents the metadata for a bean properties and actions. Metadata for beans is derived automatically by convention and optionally 
0065  * a number of different explicit sources. See the documentation for more information.
0066  <p/>
0067  *  
0068  @author Dan Syrstad
0069  */
0070 public class BeanMetaData extends MetaData implements Serializable
0071 {
0072     private static final long serialVersionUID = -4705317346444856939L;
0073 
0074     private static Logger logger = Logger.getLogger(BeanMetaData.class.getName());
0075 
0076     private static final Class<?>[] PROP_CHANGE_LISTENER_ARG = new Class<?>[] { PropertyChangeListener.class };
0077     /** Cache of beanprops files, already parsed. Key is the beanprops name, value is a List of Beans. */
0078     private static final Map<String, CachedBeanProps> cachedBeanProps = new HashMap<String, CachedBeanProps>();
0079     private static final String DEFAULT_RESOURCE_KEY = "STUB"
0080 
0081     public static final String PARAM_VIEW_ONLY = "viewOnly";
0082     public static final String PARAM_DISPLAYED = "displayed";
0083     public static final String PARAM_TABS = "tabs";
0084     public static final String PARAM_PROPS = "props";
0085     public static final String PARAM_ACTIONS = "actions";
0086     public static final String PARAM_LABEL = "label";
0087     public static final String PARAM_CONTAINER = "container";
0088     public static final String PARAM_CSS = "css";
0089     public static final String PARAM_DYNAMIC_CSS = "dynamicCss";
0090 
0091     public static final String TAB_PROPERTY_PREFIX = "tab.";
0092     public static final String ACTION_PROPERTY_PREFIX = "action.";
0093     public static final String DEFAULT_TAB_ID = "DEFAULT_TAB";
0094     
0095     private Class<?> beanClass;
0096     private Class<?> metaDataClass;
0097     private Beans beansMetaData;
0098     private List<Bean> collectedBeans = new ArrayList<Bean>();
0099     private String context;
0100     private Component component;
0101     private ComponentRegistry componentRegistry;
0102     private boolean isChildBean;
0103 
0104     // List of all properties.
0105     private List<ElementMetaData> elements = new ArrayList<ElementMetaData>();
0106     private List<TabMetaData> tabs = new ArrayList<TabMetaData>();
0107 
0108     private boolean hasAddPropertyChangeListenerMethod;
0109     private boolean hasRemovePropertyChangeListenerMethod;
0110 
0111     /**
0112      * Construct a BeanMetaData. 
0113      *
0114      @param beanClass the bean's class.
0115      @param context specifies a context to use when looking up beans in beanprops. May be null to not
0116      *  use a context.
0117      @param component the component used to get the Localizer.
0118      @param componentRegistry the ComponentRegistry used to determine visual components. May be null.
0119      @param viewOnly if true, specifies that the entire bean is view-only. This can be overridden by the
0120      *  Localizer configuration.
0121      */
0122     public BeanMetaData(Class<?> beanClass, String context, Component component, ComponentRegistry componentRegistry,
0123                     boolean viewOnly)
0124     {
0125         this(beanClass, context, null, null, component, componentRegistry, viewOnly, false);
0126     }
0127 
0128     /**
0129      * Construct a BeanMetaData. 
0130      *
0131      @param beanClass the bean's class.
0132      @param context specifies a context to use when looking up beans in beanprops. May be null to not
0133      *  use a context.
0134      @param metaDataClass an optional arbitrary class that has WWB {@link Beans} and/or {@link Bean} annotations.
0135      *  May be null. This allows bean metadata to be separate from the component and the bean, hence reusable.
0136      @param component the component used to get the Localizer.
0137      @param componentRegistry the ComponentRegistry used to determine visual components. May be null.
0138      @param viewOnly if true, specifies that the entire bean is view-only. This can be overridden by the
0139      *  Localizer configuration.
0140      */
0141     public BeanMetaData(Class<?> beanClass, String context, Class<?> metaDataClass, Component component, ComponentRegistry componentRegistry,
0142                     boolean viewOnly)
0143     {
0144         this(beanClass, context, null, metaDataClass, component, componentRegistry, viewOnly, false);
0145     }
0146 
0147     /**
0148      * Construct a BeanMetaData. 
0149      *
0150      @param beanClass the bean's class.
0151      @param context specifies a context to use when looking up beans in beanprops. May be null to not
0152      *  use a context.
0153      @param beans an implementation using the Beans annotation to provide meta data. May be null. 
0154      @param component the component used to get the Localizer.
0155      @param componentRegistry the ComponentRegistry used to determine visual components. May be null.
0156      @param viewOnly if true, specifies that the entire bean is view-only. This can be overridden by the
0157      *  Localizer configuration.
0158      */
0159     public BeanMetaData(Class<?> beanClass, String context, Beans beans, Component component, ComponentRegistry componentRegistry,
0160                     boolean viewOnly)
0161     {
0162         this(beanClass, context, beans, null, component, componentRegistry, viewOnly, false);
0163     }
0164 
0165     /**
0166      * Construct a BeanMetaData. 
0167      *
0168      @param beanClass the bean's class.
0169      @param context specifies a context to use when looking up beans in beanprops. May be null to not
0170      *  use a context.
0171      @param bean an implementation using the Bean annotation to provide meta data. May be null. 
0172      @param component the component used to get the Localizer.
0173      @param componentRegistry the ComponentRegistry used to determine visual components. May be null.
0174      @param viewOnly if true, specifies that the entire bean is view-only. This can be overridden by the
0175      *  Localizer configuration.
0176      */
0177     public BeanMetaData(Class<?> beanClass, String context, Bean bean, Component component, ComponentRegistry componentRegistry,
0178                     boolean viewOnly)
0179     {
0180         this(beanClass, context, bean == null null new JBeans(bean), null, component, componentRegistry, viewOnly, false);
0181     }
0182 
0183     /**
0184      * Construct a BeanMetaData. 
0185      *
0186      @param beanClass the bean's class.
0187      @param context specifies a context to use when looking up beans in beanprops. May be null to not
0188      *  use a context.
0189      @param beans an implementation using the Beans annotation to provide meta data.  May be null. 
0190      @param metaDataClass an optional arbitrary class that has WWB {@link Beans} and/or {@link Bean} annotations.
0191      *  May be null. This allows bean metadata to be separate from the component and the bean, hence reusable.
0192      @param component the component used to get the Localizer.
0193      @param componentRegistry the ComponentRegistry used to determine visual components. May be null.
0194      @param viewOnly if true, specifies that the entire bean is view-only. This can be overridden by the
0195      *  Localizer configuration.
0196      @param isChildBean true if this bean is a child of another bean.
0197      */
0198     public BeanMetaData(Class<?> beanClass, String context, Beans beans, Class<?> metaDataClass, Component component, ComponentRegistry componentRegistry,
0199                     boolean viewOnly, boolean isChildBean)
0200     {
0201         super(component);
0202         
0203         this.beanClass = beanClass;
0204         this.context = context;
0205         this.beansMetaData = beans;
0206         this.metaDataClass = metaDataClass;
0207         this.component = component;
0208         if (componentRegistry == null) {
0209             this.componentRegistry = new ComponentRegistry();
0210         }
0211         else {
0212             this.componentRegistry = componentRegistry;
0213         }
0214 
0215         this.isChildBean = isChildBean;
0216 
0217         setParameter(PARAM_VIEW_ONLY, String.valueOf(viewOnly));
0218         setParameter(PARAM_DISPLAYED, "true");
0219         
0220         String beanClassName = getBaseClassName(beanClass);
0221         String label = getLabelFromLocalizer(beanClassName, beanClassName);
0222         if (label == null) {
0223             label = createLabel(beanClassName);
0224         }
0225         setParameter(PARAM_LABEL, label);
0226 
0227         init();
0228 
0229         consumeParameter(PARAM_LABEL);
0230         consumeParameter(PARAM_ACTIONS);
0231         consumeParameter(PARAM_PROPS);
0232         consumeParameter(PARAM_TABS);
0233         consumeParameter(PARAM_DISPLAYED);
0234         consumeParameter(PARAM_VIEW_ONLY);
0235     }
0236 
0237     /**
0238      * Determines if all parameters specified have been consumed for a specific tab, or all tabs.
0239      
0240      @param unconsumedMsgs messages that report the parameter keys that were specified but not consumed.
0241      @param tabMetaData the tab to be checked. If null, all elements and tabs are checked.
0242      
0243      @return true if all parameters specified have been consumed.
0244      */
0245     public boolean areAllParametersConsumed(Set<String> unconsumedMsgs, TabMetaData tabMetaData)
0246     {
0247         if (!super.areAllParametersConsumed("Bean " + beanClass.getName(), unconsumedMsgs)) {
0248             return false;
0249         }
0250 
0251         // Make sure all elements and tabs have their parameters consumed.
0252         for (ElementMetaData element : tabMetaData == null ? getDisplayedElements() : getTabElements(tabMetaData)) {
0253             if (!element.areAllParametersConsumed("Property " + element.getPropertyName(), unconsumedMsgs)) {
0254                 return false;
0255             }
0256         }
0257 
0258         for (TabMetaData tab : tabMetaData == null ? tabs : Collections.singletonList(tabMetaData)) {
0259             if (!tab.areAllParametersConsumed("Tab " + tab.getId(), unconsumedMsgs)) {
0260                 return false;
0261             }
0262         }
0263 
0264         return true;
0265     }
0266 
0267     /**
0268      * Logs a warning if any parameter specified have not been consumed for a specific tab, or all tabs.
0269      
0270      @param tabMetaData the tab to be checked. If null, all elements and tabs are checked.
0271      */
0272     public void warnIfAnyParameterNotConsumed(TabMetaData tabMetaData)
0273     {
0274         Set<String> msgs = new HashSet<String>();
0275         if (!areAllParametersConsumed(msgs, tabMetaData)) {
0276             for (String msg : msgs) {
0277                 logger.warning(msg);
0278             }
0279         }
0280     }
0281 
0282     private Method getAddPropertyChangeListenerMethod()
0283     {
0284         try {
0285             return beanClass.getMethod("addPropertyChangeListener", PROP_CHANGE_LISTENER_ARG);
0286         }
0287         catch (Exception e) {
0288             // Assume we don't have it.
0289             return null;
0290         }
0291     }
0292 
0293     private Method getRemovePropertyChangeListenerMethod()
0294     {
0295         try {
0296             return beanClass.getMethod("removePropertyChangeListener", PROP_CHANGE_LISTENER_ARG);
0297         }
0298         catch (Exception e) {
0299             // Assume we don't have it.
0300             return null;
0301         }
0302     }
0303 
0304     private void init()
0305     {
0306         // Check if bean supports PropertyChangeListeners.
0307         hasAddPropertyChangeListenerMethod = getAddPropertyChangeListenerMethod() != null;
0308         hasRemovePropertyChangeListenerMethod = getRemovePropertyChangeListenerMethod() != null;
0309         
0310         String baseBeanClassName = getBaseClassName(beanClass);
0311 
0312         // Deduce actions from the component.
0313         List<Method> actionMethods = getActionMethods(component.getClass());
0314         for (Method method : actionMethods) {
0315             String name = method.getName();
0316             String prefixedName = ACTION_PROPERTY_PREFIX + name;
0317             String label = getLabelFromLocalizer(baseBeanClassName, prefixedName);
0318             if (label == null) {
0319                 label = createLabel(name);
0320             }
0321             
0322             ElementMetaData actionMeta = new ElementMetaData(this, prefixedName, label, null);
0323             actionMeta.setAction(true);
0324             elements.add(actionMeta);
0325         }
0326         
0327         // Create defaults based on the bean itself.
0328         PropertyDescriptor[] descriptors = PropertyUtils.getPropertyDescriptors(beanClass);
0329         for (PropertyDescriptor descriptor : descriptors) {
0330             String name = descriptor.getName();
0331             
0332             // Skip getClass() and methods that are not readable or hidden.
0333             if (name.equals("class"|| descriptor.getReadMethod() == null || descriptor.isHidden()) {
0334                 continue;
0335             }
0336             
0337             String label = getLabelFromLocalizer(baseBeanClassName, name);
0338             if (label == null) {
0339                 label = descriptor.getDisplayName();
0340             }
0341             
0342             if (label.equals(name)) {
0343                 label = createLabel(name);
0344             }
0345 
0346             ElementMetaData propertyMeta = new ElementMetaData(this, name, label, descriptor.getPropertyType());
0347             propertyMeta.setViewOnlyisViewOnly() );
0348             elements.add(propertyMeta);
0349 
0350             if (descriptor.getWriteMethod() == null) {
0351                 propertyMeta.setViewOnly(true);
0352             }
0353             
0354             deriveElementFromAnnotations(descriptor, propertyMeta);
0355         }
0356 
0357         // Collect various sources of metadata for the bean we're interested in. 
0358         collectAnnotations();
0359         collectFromBeanProps();
0360         collectBeansAnnotation(beansMetaData, false);
0361         
0362         // Process action annotations on component.
0363         for (Method method : getActionMethods(component.getClass())) {
0364             Action action = method.getAnnotation(Action.class);
0365             processActionAnnotation(action, method.getName());
0366         }
0367 
0368         // Determine the hierarchy of Bean contexts. I.e., the default Bean is always processed first, followed by those that
0369         // extend it, etc. This acts as a stack.
0370         List<Bean> beansHier = buildContextStack();
0371         
0372         // Apply beans in order from highest to lowest. The default context will always be first.
0373         boolean foundSpecifiedContext = false;
0374         for (Bean bean : beansHier) {
0375             if (context != null && context.equals(bean.context())) {
0376                 foundSpecifiedContext = true;
0377             }
0378             
0379             processBeanAnnotation(bean);
0380         }
0381 
0382         // Ensure that if a context was specified, that we found one in the metadata. Otherwise it might have been a typo.
0383         if (context != null && !foundSpecifiedContext) {
0384             throw new RuntimeException("Could not find specified context '" + context + "' in metadata.");
0385         }
0386         
0387         // Post-process Bean-level parameters
0388         if (!getBooleanParameter(PARAM_DISPLAYED)) {
0389             elements.clear();
0390             tabs.clear();
0391         }
0392         
0393         // Configure tabs
0394         if (tabs.isEmpty()) {
0395             // Create single default tab.
0396             tabs.addnew TabMetaData(this, DEFAULT_TAB_ID, getParameter(PARAM_LABEL) ) );
0397         }
0398         
0399         String defaultTabId = tabs.get(0).getId();
0400         
0401         // Post-process each property based on bean parameters
0402         for (ElementMetaData elementMeta : elements) {
0403             // If element is not on a tab, add it to the first. If it's an action, it must have been assigned an order to
0404             // appear on a tab. Otherwise it is a global action.
0405             if (elementMeta.getTabId() ==  null &&
0406                 (!elementMeta.isAction() || 
0407                  (elementMeta.isAction() && elementMeta.isActionSpecifiedInProps()))) {
0408                 elementMeta.setTabId(defaultTabId);
0409             }
0410         }
0411 
0412         Collections.sort(elements, new Comparator<ElementMetaData>() {
0413             public int compare(ElementMetaData o1, ElementMetaData o2)
0414             {
0415                 return (o1.getOrder() > o2.getOrder() (o1.getOrder() < o2.getOrder() ? -0));
0416             }
0417         });
0418     }
0419 
0420     /**
0421      * Determine the hierarchy of Bean contexts. I.e., the default Bean is always processed first, followed by those that
0422      * extend it, etc. This acts as a stack.
0423      *
0424      @return see above.
0425      */
0426     private List<Bean> buildContextStack()
0427     {
0428         List<Bean> beansHier = new ArrayList<Bean>();
0429         String currContext = context;
0430         
0431         // Note: Limit accidental cyclical specs (e.g., A extends B, B extends A). This also limits the maximum hierarchy depth to the same 
0432         // amount, which should be plenty.
0433         for (int limit = 0; limit < 20; ++limit) {
0434             String extendsContext = null;
0435             for (Bean collectedBean : collectedBeans) {
0436                 String beanContext = collectedBean.context();
0437                 if (beanContext != null && beanContext.length() == 0) {
0438                     beanContext = null;
0439                 }
0440                 
0441                 if (beanContext == currContext || (beanContext != null && beanContext.equals(currContext))) {
0442                     // Push it on stack
0443                     beansHier.add(0, collectedBean);
0444                     
0445                     if (extendsContext == null) {
0446                         extendsContext = collectedBean.extendsContext();
0447                         if (extendsContext != null && extendsContext.length() == 0) {
0448                             extendsContext = null;
0449                         }
0450                     }
0451                     else if (!extendsContext.equals(collectedBean.extendsContext())) {
0452                         throw new RuntimeException("Inconsistent extends context " + collectedBean.extendsContext() 
0453                                         " is not consistent with first one encountered " + extendsContext);
0454                     }
0455                 }
0456             }
0457 
0458             if (currContext == null || currContext.length() == 0) {
0459                 // Just processed the default context, so stop.
0460                 break;
0461             }
0462 
0463             currContext = extendsContext;
0464         }
0465         return beansHier;
0466     }
0467 
0468     /**
0469      * Attempts to get the label for the given action or property name from the Localizer.
0470      *
0471      @param baseBeanClassName
0472      @param name
0473      
0474      @return the label, or null if not defined.
0475      */
0476     private String getLabelFromLocalizer(String baseBeanClassName, String name)
0477     {
0478         // Try to retrieve label from properties file in the form of "Bean.{name}.label" or
0479         // simply {name}.label.
0480         String propLabelKey = name + ".label";
0481         String label = component.getLocalizer().getString(baseBeanClassName + '.' + propLabelKey, component, DEFAULT_RESOURCE_KEY);
0482         if (label == DEFAULT_RESOURCE_KEY) {
0483             label = component.getLocalizer().getString(propLabelKey, component, DEFAULT_RESOURCE_KEY);
0484         }
0485         
0486         if (label == DEFAULT_RESOURCE_KEY) {
0487             label = null;
0488         }
0489         
0490         return label;
0491     }
0492     
0493     /**
0494      * Collect any WWB Beans or Bean annotations that may exist on the component, bean, or meta-data class that
0495      * apply to the specified bean.
0496      * Order of processing is: Bean, Metadata class, then Component. Hence, Component annotations
0497      * augment or override those of the Metadata class and the Bean.  
0498      */
0499     private void collectAnnotations()
0500     {
0501         // Bean itself
0502         collectBeansAnnotationbeanClass.getAnnotation(Beans.class)true);
0503         collectBeanAnnotationbeanClass.getAnnotation(Bean.class)true);
0504 
0505         // Metadata class
0506         if (metaDataClass != null) {
0507             collectBeansAnnotationmetaDataClass.getAnnotation(Beans.class)false);
0508             collectBeanAnnotationmetaDataClass.getAnnotation(Bean.class)false);
0509         }
0510         
0511         // Component
0512         Class<? extends Component> componentClass = component.getClass()
0513         collectBeansAnnotationcomponentClass.getAnnotation(Beans.class)false);
0514         collectBeanAnnotationcomponentClass.getAnnotation(Bean.class)false);
0515     }
0516     
0517     private void collectBeansAnnotation(Beans beans, boolean isBeanAnnotation)
0518     {
0519         if (beans != null) {
0520             for (Bean bean : beans.value()) {
0521                 collectBeanAnnotation(bean, isBeanAnnotation);
0522             }
0523         }
0524     }
0525     
0526     private void collectBeanAnnotation(Bean bean, boolean isBeanAnnotation)
0527     {
0528         if (bean == null) {
0529             return;
0530         }
0531         
0532         Class<?> beanType = bean.type();
0533         if (beanType == Object.class) {
0534             if (!isBeanAnnotation) {
0535                 throw new RuntimeException("@Bean must include the type attribute when used on non-bean components. Occurred while processing annotations for bean " 
0536                                 + beanClass.getName());
0537             }
0538             
0539             beanType = beanClass;
0540         }
0541         
0542         if (beanType != beanClass) {
0543             return// Doesn't match what we're interested in.
0544         }
0545         
0546         collectedBeans.add(bean);
0547     }
0548 
0549     private void processBeanAnnotation(Bean bean)
0550     {
0551         setParameter(BeanGridPanel.PARAM_COLS, String.valueOf(bean.columns()));
0552         setParameter(PARAM_DISPLAYED, String.valueOf(bean.displayed()));
0553         setParameterIfNotEmpty(PARAM_LABEL, bean.label());
0554         if (bean.container() != Panel.class) {
0555             setParameter(PARAM_CONTAINER, bean.container().getName());
0556         }
0557         
0558         setParameter(BeanForm.PARAM_ROWS, String.valueOf(bean.rows()));
0559         
0560         setParameter(PARAM_CSS, bean.css());
0561         setParameter(PARAM_DYNAMIC_CSS, bean.dynamicCss());
0562         
0563         if (bean.viewOnly().length > 0) {
0564             // Only set if explicitly set.
0565             boolean viewOnly = bean.viewOnly()[0];
0566             setParameter(PARAM_VIEW_ONLY, String.valueOf(viewOnly));
0567             updateElementsViewOnlyState(viewOnly);
0568         }
0569 
0570         setParameterIfNotEmpty(bean.paramName(), bean.paramValue());
0571         for (net.sourceforge.wicketwebbeans.annotations.Parameter param : bean.params()) {
0572             setParameterIfNotEmpty(param.name(), param.value());
0573             if (param.name().equals(PARAM_VIEW_ONLY)) {
0574                 updateElementsViewOnlyStategetBooleanParameter(PARAM_VIEW_ONLY) );
0575             }
0576         }
0577         
0578         // Process actionNames after actions because actionNames is typically used to define order.
0579         int order = 1;
0580         for (String actionName : bean.actionNames()) {
0581             if (!handleElementRemove(actionName, true)) {
0582                 ElementMetaData element = findElementAddPseudos(ACTION_PROPERTY_PREFIX + actionName);
0583                 if (!element.isActionSpecifiedInProps() && element.getOrder() == ElementMetaData.DEFAULT_ORDER) {
0584                     element.setOrder(order++);
0585                 }
0586             }
0587         }
0588         
0589         order = 1;
0590         for (Action action : bean.actions()) {
0591             if (!handleElementRemove(action.name()false)) {
0592                 ElementMetaData element = processActionAnnotation(action, null);
0593                 if (!element.isActionSpecifiedInProps() && element.getOrder() == ElementMetaData.DEFAULT_ORDER) {
0594                     element.setOrder(order++);
0595                 }
0596             }
0597         }
0598 
0599         // Process propertyNames before properties because propertyNames is typically used to define order.
0600         order = 1;
0601         for (String propName : bean.propertyNames()) {
0602             if (!handleElementRemove(propName, false)) {
0603                 ElementMetaData element = findElementAddPseudos(propName);
0604                 if (element.isAction()) {
0605                     element.setActionSpecifiedInProps(true);
0606                 }
0607                 
0608                 if (element.getOrder() == ElementMetaData.DEFAULT_ORDER) {
0609                     element.setOrder(order++);
0610                 }
0611             }
0612         }
0613         
0614         order = 1;
0615         for (Property property : bean.properties()) {
0616             if (!handleElementRemove(property.name()false)) {
0617                 ElementMetaData element = processPropertyAnnotation(property, null);
0618                 if (element.isAction()) {
0619                     element.setActionSpecifiedInProps(true);
0620                 }
0621                 
0622                 if (element.getOrder() == ElementMetaData.DEFAULT_ORDER) {
0623                     element.setOrder(order++);
0624                 }
0625             }
0626         }
0627 
0628         for (Tab tab : bean.tabs()) {
0629             String tabName = tab.name();
0630             boolean removeTab = false;
0631             if (tabName.startsWith("-"&& tabName.length() 1) {
0632                 tabName = tabName.substring(1);
0633                 removeTab = true;
0634             }
0635 
0636             TabMetaData foundTab = findTab(tabName);
0637 
0638             if (removeTab) {
0639                 if (foundTab == null) {
0640                     throw new RuntimeException("Tab " + tabName + " does not exist in exposed list of tabs.");
0641                 }
0642 
0643                 tabs.remove(foundTab);
0644             }
0645             else {
0646                 processTabAnnotation(tab, foundTab);
0647             }
0648         }
0649     }
0650 
0651     /**
0652      * Set all elements to same viewOnly state. Note that this happens before individual elements are processed so 
0653      * that they can override the bean setting if necessary.
0654      *
0655      @param viewOnly
0656      */
0657     private void updateElementsViewOnlyState(boolean viewOnly)
0658     {
0659         for (ElementMetaData element : elements) {
0660             element.setViewOnly(viewOnly);
0661         }
0662     }
0663     
0664     /**
0665      * Handle element removal if element name starts with a '-'. 
0666      *
0667      @param elementName the element name, possibly starting with '-'.
0668      
0669      @return true if element was removed, else false.
0670      */
0671     private boolean handleElementRemove(String elementName, boolean prependActionPrefix)
0672     {
0673         if (elementName.startsWith("-"&& elementName.length() 1) {
0674             elementName = elementName.substring(1);
0675             if (prependActionPrefix) {
0676                 elementName = ACTION_PROPERTY_PREFIX + elementName;
0677             }
0678             
0679             elements.removefindElementAddPseudos(elementName) );
0680             return true;
0681         }
0682         
0683         return false;
0684     }
0685     
0686     private ElementMetaData processPropertyAnnotation(Property property, ElementMetaData element)
0687     {
0688         if (property == null) {
0689             return null;
0690         }
0691         
0692         if (element == null && property.name().length() == 0) {
0693             throw new RuntimeException("@Property annotation of @Bean " + beanClass.getName() " did not set the name attribute.");
0694         }
0695         
0696         if (element == null || property.name().length() 0) {
0697             element = findElementAddPseudos(property.name());
0698         }
0699         
0700         if (property.colspan() 1) {
0701             element.setParameter(BeanGridPanel.PARAM_COLSPAN, String.valueOf(property.colspan()));
0702         }
0703         
0704         if (property.rows() 0) {
0705             element.setParameter(ElementMetaData.PARAM_ROWS, String.valueOf(property.rows()));
0706         }
0707         
0708         if (property.columns() 0) {
0709             element.setParameter(ElementMetaData.PARAM_COLUMNS, String.valueOf(property.columns()));
0710         }
0711         
0712         element.setParameterIfNotEmpty(ElementMetaData.PARAM_DEFAULT_VALUE, property.defaultValue());
0713         if (property.elementType() != Object.class) {
0714             element.setParameter(ElementMetaData.PARAM_ELEMENT_TYPE, property.elementType().getName());
0715         }
0716         
0717         if (property.fieldType() != Field.class) {
0718             element.setParameter(ElementMetaData.PARAM_FIELD_TYPE, property.fieldType().getName());
0719         }
0720         
0721         element.setParameterIfNotEmpty(ElementMetaData.PARAM_LABEL, property.label());
0722         element.setParameterIfNotEmpty(ElementMetaData.PARAM_LABEL_IMAGE, property.labelImage());
0723         element.setParameter(PARAM_CSS, property.css());
0724         element.setParameter(PARAM_DYNAMIC_CSS, property.dynamicCss());
0725         
0726         if (property.maxLength() 0) {
0727             element.setMaxLength(property.maxLength());
0728         }
0729         
0730         element.setRequired(property.required());
0731         if (!element.isAction()) {
0732             // Only set viewOnly if explicitly set.
0733             if (property.viewOnly().length > 0) {
0734                 element.setViewOnly(property.viewOnly()[0]);
0735             }
0736         }
0737         
0738         element.setParameterIfNotEmpty(property.paramName(), property.paramValue());
0739         for (net.sourceforge.wicketwebbeans.annotations.Parameter param : property.params()) {
0740             element.setParameterIfNotEmpty(param.name(), param.value());
0741         }
0742         
0743         return element;
0744     }
0745     
0746     private ElementMetaData processActionAnnotation(Action action, String methodName)
0747     {
0748         if (action == null) {
0749             return null;
0750         }
0751         
0752         if (methodName == null && action.name().length() == 0) {
0753             throw new RuntimeException("@Action annotation of @Bean " + beanClass.getName() " did not set the name attribute.");
0754         }
0755         
0756         if (action.name().length() 0) {
0757             methodName = action.name();
0758         }
0759         
0760         ElementMetaData element = findElementAddPseudos(ACTION_PROPERTY_PREFIX + methodName);
0761 
0762         if (action.colspan() 1) {
0763             element.setParameter(BeanGridPanel.PARAM_COLSPAN, String.valueOf(action.colspan()));
0764         }
0765         
0766         element.setParameterIfNotEmpty(ElementMetaData.PARAM_LABEL, action.label());
0767         element.setParameterIfNotEmpty(ElementMetaData.PARAM_LABEL_IMAGE, action.labelImage());
0768         
0769         element.setParameterIfNotEmpty(BeanSubmitButton.PARAM_CONFIRM, action.confirm());
0770         element.setParameter(BeanSubmitButton.PARAM_AJAX, String.valueOf(action.ajax()));
0771         element.setParameterIfNotEmpty(BeanSubmitButton.PARAM_DEFAULT, String.valueOf(action.isDefault()));
0772         
0773         element.setParameterIfNotEmpty(action.paramName(), action.paramValue());
0774         for (net.sourceforge.wicketwebbeans.annotations.Parameter param : action.params()) {
0775             element.setParameterIfNotEmpty(param.name(), param.value());
0776         }
0777         
0778         return element;
0779     }
0780     
0781     /**
0782      * Process a Tab annotation.
0783      *
0784      @param tab the annotation.
0785      @param tabMetaData the tab metadata, if it already exists.
0786      */
0787     private void processTabAnnotation(Tab tab, TabMetaData tabMetaData)
0788     {
0789         if (tab == null) {
0790             return;
0791         }
0792         
0793         String tabName = tab.name();
0794         if (tabMetaData == null) {
0795           String baseBeanClassName = getBaseClassName(beanClass);
0796             String prefixedName = TAB_PROPERTY_PREFIX + tabName;
0797             String label = getLabelFromLocalizer(baseBeanClassName, prefixedName);
0798             if (label == null) {
0799                 label = createLabel(tabName);
0800             }
0801             
0802             tabMetaData = new TabMetaData(this, tabName, label);
0803             tabs.add(tabMetaData);
0804         }
0805         
0806         tabMetaData.setParameterIfNotEmpty(PARAM_LABEL, tab.label());
0807         
0808         int order = 1;
0809         for (Property property : tab.properties()) {
0810             if (!handleElementRemove(property.name()false)) {
0811                 ElementMetaData element = processPropertyAnnotation(property, null);
0812                 element.setTabIdtabMetaData.getId() );
0813                 element.setOrder(order++);
0814                 if (element.isAction()) {
0815                     element.setActionSpecifiedInProps(true);
0816                 }
0817             }
0818         }
0819 
0820         // Process propertyNames after properties because propertyNames is typically used to define order.
0821         order = 1;
0822         for (String propName : tab.propertyNames()) {
0823             if (!handleElementRemove(propName, false)) {
0824                 ElementMetaData element = findElementAddPseudos(propName);
0825                 element.setTabIdtabMetaData.getId() );
0826                 element.setOrder(order++);
0827                 if (element.isAction()) {
0828                     element.setActionSpecifiedInProps(true);
0829                 }
0830             }
0831         }
0832         
0833         tabMetaData.setParameterIfNotEmpty(tab.paramName(), tab.paramValue());
0834         for (net.sourceforge.wicketwebbeans.annotations.Parameter param : tab.params()) {
0835             tabMetaData.setParameterIfNotEmpty(param.name(), param.value());
0836         }
0837     }
0838     
0839     /**
0840      * Collection Beans from the beanprops file, if any.
0841      */
0842     private void collectFromBeanProps()
0843     {
0844         String propFileName = getBaseClassName(component.getClass()) ".beanprops";
0845         URL propFileURL = component.getClass().getResource(propFileName);
0846         long timestamp = 0;
0847         if (propFileURL != null && propFileURL.getProtocol().equals("file")) {
0848             try {
0849                 timestamp = new File(propFileURL.toURI()).lastModified();
0850             }
0851             catch (URISyntaxException e) { /* Ignore - treat as zero */ }
0852         }
0853         
0854         String cacheKey = beanClass.getName() ':' + propFileName;
0855         CachedBeanProps beanprops = cachedBeanProps.get(cacheKey);
0856         if (beanprops == null || beanprops.getModTimestamp() != timestamp) {
0857             if (beanprops != null) {
0858                 logger.info("File changed: " + propFileName + " re-reading.");
0859             }
0860             
0861             // It's OK not to have a beanprops file. We can deduce the parameters by convention. 
0862             InputStream propsStream = component.getClass().getResourceAsStream(propFileName);
0863             if (propsStream != null) {
0864                 try {
0865                     JBeans beans = new BeanPropsParser(propFileName, propsStream).parseToJBeans(this);
0866                     beanprops = new CachedBeanProps(beans, timestamp);
0867                     cachedBeanProps.put(cacheKey, beanprops);
0868                 }
0869                 finally {
0870                     try propsStream.close()catch (IOException e) { /* Ignore */ }
0871                 }
0872             }
0873         }
0874         
0875         if (beanprops != null) {
0876             collectBeansAnnotation(beanprops.getBeans()false);
0877         }
0878     }
0879     
0880     /**
0881      * Derive metadata from standard annotations such as JPA and FindBugs.
0882      *
0883      @param descriptor
0884      @param elementMetaData
0885      */
0886     private void deriveElementFromAnnotations(PropertyDescriptor descriptor, ElementMetaData elementMetaData)
0887     {
0888         // NOTE: !!! The annotation classes must be present at runtime, otherwise getAnnotations() doesn't 
0889         // return the annotation.
0890         Method readMethod = descriptor.getReadMethod();
0891         if (readMethod != null) {
0892             processElementAnnotations(elementMetaData, readMethod.getAnnotations());
0893         }
0894 
0895         Method writeMethod = descriptor.getWriteMethod();
0896         if (writeMethod != null) {
0897             processElementAnnotations(elementMetaData, writeMethod.getAnnotations());
0898         }
0899     }
0900     
0901     /**
0902      * Process annotations for {@link #deriveElementFromAnnotations(PropertyDescriptor, ElementMetaData)}.
0903      *
0904      @param elementMetaData
0905      @param annotations
0906      */
0907     private void processElementAnnotations(ElementMetaData elementMetaData, Annotation[] annotations)
0908     {
0909         if (annotations == null) {
0910             return;
0911         }
0912         
0913         // Note: We only reference the annotations using their string name, not the class.
0914         // If we referenced the class, we'd have a dependency on those classes.
0915         // We also have to access the values by reflection so we don't depend on the class.
0916         for (Annotation annotation : annotations) {
0917             Class<?> annotationType = annotation.annotationType();
0918             String name = annotationType.getName();
0919             
0920             if (name.equals("javax.persistence.Column")) {
0921                 elementMetaData.setMaxLength( (Integer)invokeAnnotationMethod(annotation, "length") );
0922                 elementMetaData.setRequired!(Boolean)invokeAnnotationMethod(annotation, "nullable") );
0923             }
0924             else if (name.equals("javax.jdo.annotations.Column")) {
0925                 elementMetaData.setMaxLength( (Integer)invokeAnnotationMethod(annotation, "length") );
0926                 elementMetaData.setRequired"false".equals((String)invokeAnnotationMethod(annotation, "allowsNull")) );
0927                 elementMetaData.setDefaultValue( (String)invokeAnnotationMethod(annotation, "defaultValue") );
0928             }
0929             else if (annotationType == Property.class) {
0930                 processPropertyAnnotation((Property)annotation, elementMetaData);
0931             }
0932         }
0933     }
0934     
0935     /**
0936      * Invokes an annotation method to get a value, possibly returning null if no value or if the method doesn't exist.
0937      */
0938     private Object invokeAnnotationMethod(Annotation annotation, String methodName)
0939     {
0940         try {
0941             return MethodUtils.invokeExactMethod(annotation, methodName, null);
0942         }
0943         catch (Exception e) {
0944             // Ignore.
0945             return null;
0946         }
0947     }
0948 
0949     /**
0950      * Find action methods for a class. 
0951      *
0952      @param aClass the class.
0953      
0954      @return an List of sorted action methods, possibly empty.
0955      */
0956     private List<Method> getActionMethods(Class<? extends Component> aClass)
0957     {
0958         List<Method> result = new ArrayList<Method>();
0959         for (Method method : aClass.getMethods()) {
0960             Class<?>[] params = method.getParameterTypes();
0961             Class<?> returnType = method.getReturnType();
0962             if (returnType.equals(Void.TYPE&& params.length == &&
0963                 params[0== AjaxRequestTarget.class &&
0964                 params[1== Form.class &&
0965                 (params[2== beanClass || params[2== Object.class)) {
0966                 result.add(method);
0967             }
0968         }
0969         
0970         Collections.sort(result, new Comparator<Method>() {
0971             public int compare(Method o1, Method o2)
0972             {
0973                 return o1.getName().compareTo(o2.getName());
0974             }
0975             
0976         });
0977         return result;
0978     }
0979 
0980     /**
0981      * Gets the base class name of a Class.
0982      
0983      @param aClass the class.
0984      *
0985      @return the base class name (the name without the package name).
0986      */
0987     static String getBaseClassName(Class<?> aClass)
0988     {
0989         String baseClassName = aClass.getName();
0990         int idx = baseClassName.lastIndexOf('.');
0991         if (idx >= 0) {
0992             baseClassName = baseClassName.substring(idx + 1);
0993         }
0994 
0995         return baseClassName;
0996     }
0997 
0998     /**
0999      * Finds a tab.
1000      *
1001      @param tabName the tab name
1002      @return the TabMetaData, or null if not found.
1003      */
1004     private TabMetaData findTab(String tabName)
1005     {
1006         TabMetaData foundTab = null;
1007         for (TabMetaData tab : tabs) {
1008             if (tab.getId().equals(tabName)) {
1009                 foundTab = tab;
1010                 break;
1011             }
1012         }
1013         return foundTab;
1014     }
1015 
1016     /**
1017      * Finds the specified element in the list of all elements. Handles special
1018      * Pseudo property names (e.g., "EMPTY") by adding a new one to the list.
1019      
1020      @param propertyName
1021      
1022      @return the ElementMetaData.
1023      
1024      @throws RuntimeException if property is not found.
1025      */
1026     private ElementMetaData findElementAddPseudos(String propertyName)
1027     {
1028         ElementMetaData prop;
1029         if (propertyName.equals("EMPTY")) {
1030             prop = new ElementMetaData(this, "EMPTY:" + elements.size()"", Object.class);
1031             prop.setFieldType(EmptyField.class.getName());
1032             prop.setViewOnly(true);
1033             elements.add(prop);
1034         }
1035         else {
1036             prop = findElement(propertyName);
1037             if (prop == null) {
1038                 throw new RuntimeException("Property: " + propertyName
1039                                 " does not exist in exposed list of properties.");
1040             }
1041         }
1042 
1043         return prop;
1044     }
1045 
1046     /**
1047      * Finds the specified element in the list of all elements.
1048      
1049      @param propertyName
1050      
1051      @return the ElementMetaData or null if not found.
1052      */
1053     public ElementMetaData findElement(String propertyName)
1054     {
1055         for (ElementMetaData prop : elements) {
1056             if (prop.getPropertyName().equals(propertyName)) {
1057                 return prop;
1058             }
1059         }
1060 
1061         return null;
1062     }
1063 
1064     /**
1065      * Creates a human readable label from a Java identifier.
1066      
1067      @param identifier the Java identifier.
1068      
1069      @return the label.
1070      */
1071     private static String createLabel(String identifier)
1072     {
1073         // Check for a complex property.
1074         int idx = identifier.lastIndexOf('.');
1075         if (idx < 0) {
1076             idx = identifier.lastIndexOf('$')// Java nested classes.
1077         }
1078 
1079         if (idx >= && identifier.length() 1) {
1080             identifier = identifier.substring(idx + 1);
1081         }
1082 
1083         if (identifier.length() == 0) {
1084             return "";
1085         }
1086 
1087         char[] chars = identifier.toCharArray();
1088         StringBuffer buf = new StringBuffer(chars.length + 10);
1089 
1090         // Capitalize the first letter.
1091         buf.append(Character.toUpperCase(chars[0]));
1092         boolean lastLower = false;
1093         for (int i = 1; i < chars.length; ++i) {
1094             if (!Character.isLowerCase(chars[i])) {
1095                 // Lower to upper case transition -- add space before it
1096                 if (lastLower) {
1097                     buf.append(' ');
1098                 }
1099             }
1100 
1101             buf.append(chars[i]);
1102             lastLower = Character.isLowerCase(chars[i]) || Character.isDigit(chars[i]);
1103         }
1104 
1105         return buf.toString();
1106     }
1107 
1108     public String getLabel()
1109     {
1110         return getParameter(PARAM_LABEL);
1111     }
1112     
1113     public Class<? extends Panel> getContainerClass()
1114     {
1115         String container = getParameter(PARAM_CONTAINER);
1116         if (container == null) {
1117             return null;
1118         }
1119 
1120         try {
1121             return (Class<? extends Panel>)Class.forName(container);
1122         }
1123         catch (Exception e) {
1124             throw new RuntimeException("Cannot load container class " + container);
1125         }
1126     }
1127 
1128     /**
1129      @return the tabs defined for this bean. There will always be at least one tab.
1130      */
1131     public List<TabMetaData> getTabs()
1132     {
1133         return tabs;
1134     }
1135 
1136     /**
1137      @return a list of all displayed elements for a tab.
1138      */
1139     public List<ElementMetaData> getTabElements(TabMetaData tab)
1140     {
1141         List<ElementMetaData> elems = new ArrayList<ElementMetaData>();
1142         for (ElementMetaData elem : elements) {
1143             if (elem.getTabId() != null && elem.getTabId().equals(tab.getId())) {
1144                 elems.add(elem);
1145             }
1146         }
1147 
1148         return elems;
1149     }
1150 
1151     /**
1152      @return a list of all displayed elements for a bean.
1153      */
1154     public List<ElementMetaData> getDisplayedElements()
1155     {
1156         return elements;
1157     }
1158 
1159     /**
1160      * Gets a list of actions that are not assigned to any particular placement within the bean.
1161      *
1162      @return the list of global actions.
1163      */
1164     public List<ElementMetaData> getGlobalActions()
1165     {
1166         List<ElementMetaData> elems = new ArrayList<ElementMetaData>();
1167         for (ElementMetaData elem : elements) {
1168             if (elem.isAction() && !elem.isActionSpecifiedInProps()) {
1169                 elems.add(elem);
1170             }
1171         }
1172 
1173         return elems;
1174     }
1175 
1176     /**
1177      @return the bean class.
1178      */
1179     public Class<?> getBeanClass()
1180     {
1181         return beanClass;
1182     }
1183 
1184     /**
1185      * Gets the external metadata Class supplied to the constructor.
1186      *
1187      @return a Class<?>, or null if not defined.
1188      */
1189     public Class<?> getMetaDataClass()
1190     {
1191         return metaDataClass;
1192     }
1193 
1194     /**
1195      * Gets the beansMetaData.
1196      *
1197      @return a Beans.
1198      */
1199     public Beans getBeansMetaData()
1200     {
1201         return beansMetaData;
1202     }
1203 
1204     /**
1205      @return the component.
1206      */
1207     public Component getComponent()
1208     {
1209         return component;
1210     }
1211 
1212     /**
1213      @return the componentRegistry.
1214      */
1215     public ComponentRegistry getComponentRegistry()
1216     {
1217         return componentRegistry;
1218     }
1219 
1220     /**
1221      @return the context.
1222      */
1223     public String getContext()
1224     {
1225         return context;
1226     }
1227 
1228     /**
1229      @return the viewOnly flag.
1230      */
1231     public boolean isViewOnly()
1232     {
1233         return getBooleanParameter(PARAM_VIEW_ONLY);
1234     }
1235 
1236     /**
1237      @return the displayed flag.
1238      */
1239     public boolean isDisplayed()
1240     {
1241         return getBooleanParameter(PARAM_DISPLAYED);
1242     }
1243 
1244     /**
1245      * Adds a property change listener to the bean if it supports it. If it doesn't support
1246      * addition property change listeners, nothing happens.
1247      *
1248      @param beanModel the bean's IModel.
1249      @param listener the {@link PropertyChangeListener}.
1250      */
1251     public void addPropertyChangeListener(BeanPropertyModel beanModel, PropertyChangeListener listener)
1252     {
1253         if (!hasAddPropertyChangeListenerMethod) {
1254             return;
1255         }
1256 
1257         Object bean = beanModel.getBean();
1258         if (bean != null) {
1259             try {
1260                 getAddPropertyChangeListenerMethod().invoke(bean, new Object[] { listener });
1261             }
1262             catch (Exception e) {
1263                 throw new RuntimeException("Error adding PropertyChangeListener: ", e);
1264             }
1265         }
1266     }
1267 
1268     /**
1269      * Removes a property change listener to the bean if it supports it. If it doesn't support
1270      * removal of property change listeners, nothing happens.
1271      *
1272      @param beanModel the bean's IModel.
1273      @param listener the {@link PropertyChangeListener}.
1274      */
1275     public void removePropertyChangeListener(IModel beanModel, PropertyChangeListener listener)
1276     {
1277         if (!hasRemovePropertyChangeListenerMethod) {
1278             return;
1279         }
1280 
1281         Object bean = beanModel.getObject();
1282         if (bean != null) {
1283             try {
1284                 getRemovePropertyChangeListenerMethod().invoke(bean, new Object[] { listener });
1285             }
1286             catch (Exception e) {
1287                 throw new RuntimeException("Error removing PropertyChangeListener: ", e);
1288             }
1289         }
1290     }
1291     
1292     /**
1293      * Applies any metadata-based CSS classes for the given bean or property to the component.
1294      */
1295     public void applyCss(Object bean, MetaData metaData, Component applyToComponent)
1296     {
1297         String css = metaData.getParameter(PARAM_CSS);
1298         
1299         if (!Strings.isEmpty(css)) {
1300             applyToComponent.addnew AttributeAppender("class"new Model(css)" ") );
1301         }
1302         
1303         String dynamicCssMethod = metaData.getParameter(PARAM_DYNAMIC_CSS);
1304         if (!Strings.isEmpty(dynamicCssMethod)) {
1305             try {
1306                 Method method = component.getClass().getMethod(dynamicCssMethod, new Class[] { beanClass, metaData.getClass() } );
1307                 String cssReturn = (String)method.invoke(component, new Object[] { bean, metaData });
1308                 if (!Strings.isEmpty(cssReturn)) {
1309                     applyToComponent.addnew AttributeAppender("class"new Model(cssReturn)" ") );
1310                 }
1311             }
1312             catch (Exception e) {
1313                 throw new RuntimeException("dynamicCss method " + dynamicCssMethod + "(" + beanClass.getName() ", " + metaData.getClass().getName() ") is not defined in " + applyToComponent.getClass());
1314             }            
1315         }
1316     }
1317     
1318     /**
1319      * A Cached Beanprops file.
1320      */
1321     private static final class CachedBeanProps implements Serializable
1322     {
1323         private JBeans beans;
1324         private long modTimestamp;
1325         
1326         CachedBeanProps(JBeans beans, long modTimestamp)
1327         {
1328             this.beans = beans;
1329             this.modTimestamp = modTimestamp;
1330         }
1331         
1332         JBeans getBeans()
1333         {
1334             return beans;
1335         }
1336         
1337         long getModTimestamp()
1338         {
1339             return modTimestamp;
1340         }
1341     }
1342 }
Java2html