/*
 * JBoss, Home of Professional Open Source
 *
 * Distributable under LGPL license.
 * See terms of license at gnu.org.
 */
package org.jboss.seam;

import static org.jboss.seam.ComponentType.ENTITY_BEAN;
import static org.jboss.seam.ComponentType.JAVA_BEAN;
import static org.jboss.seam.ComponentType.MESSAGE_DRIVEN_BEAN;
import static org.jboss.seam.ComponentType.STATEFUL_SESSION_BEAN;
import static org.jboss.seam.ScopeType.APPLICATION;
import static org.jboss.seam.ScopeType.CONVERSATION;
import static org.jboss.seam.ScopeType.EVENT;
import static org.jboss.seam.ScopeType.PAGE;
import static org.jboss.seam.ScopeType.SESSION;
import static org.jboss.seam.ScopeType.STATELESS;
import static org.jboss.seam.ScopeType.UNSPECIFIED;
import static org.jboss.seam.util.EJB.INTERCEPTORS;
import static org.jboss.seam.util.EJB.LOCAL;
import static org.jboss.seam.util.EJB.PERSISTENCE_CONTEXT;
import static org.jboss.seam.util.EJB.POST_ACTIVATE;
import static org.jboss.seam.util.EJB.POST_CONSTRUCT;
import static org.jboss.seam.util.EJB.PRE_DESTROY;
import static org.jboss.seam.util.EJB.PRE_PASSIVATE;
import static org.jboss.seam.util.EJB.REMOTE;
import static org.jboss.seam.util.EJB.REMOVE;
import static org.jboss.seam.util.EJB.value;

import java.io.Serializable;
import java.lang.annotation.Annotation;
import java.lang.reflect.Array;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.SortedMap;
import java.util.SortedSet;
import java.util.TreeMap;
import java.util.TreeSet;

import javassist.util.proxy.MethodFilter;
import javassist.util.proxy.MethodHandler;
import javassist.util.proxy.ProxyObject;

import javax.naming.NamingException;
import javax.servlet.http.HttpSessionActivationListener;

import org.jboss.seam.annotations.Begin;
import org.jboss.seam.annotations.Create;
import org.jboss.seam.annotations.DataBinderClass;
import org.jboss.seam.annotations.DataSelectorClass;
import org.jboss.seam.annotations.Destroy;
import org.jboss.seam.annotations.End;
import org.jboss.seam.annotations.Import;
import org.jboss.seam.annotations.In;
import org.jboss.seam.annotations.JndiName;
import org.jboss.seam.annotations.Observer;
import org.jboss.seam.annotations.Out;
import org.jboss.seam.annotations.PerNestedConversation;
import org.jboss.seam.annotations.Scope;
import org.jboss.seam.annotations.Startup;
import org.jboss.seam.annotations.Synchronized;
import org.jboss.seam.annotations.Unwrap;
import org.jboss.seam.annotations.datamodel.DataModel;
import org.jboss.seam.annotations.faces.Converter;
import org.jboss.seam.annotations.faces.Validator;
import org.jboss.seam.annotations.intercept.BypassInterceptors;
import org.jboss.seam.annotations.intercept.InterceptorType;
import org.jboss.seam.annotations.intercept.Interceptors;
import org.jboss.seam.annotations.security.PermissionCheck;
import org.jboss.seam.annotations.security.Restrict;
import org.jboss.seam.annotations.security.RoleCheck;
import org.jboss.seam.annotations.web.RequestParameter;
import org.jboss.seam.contexts.Context;
import org.jboss.seam.contexts.Contexts;
import org.jboss.seam.core.Events;
import org.jboss.seam.core.Expressions;
import org.jboss.seam.core.Init;
import org.jboss.seam.core.Mutable;
import org.jboss.seam.core.Expressions.MethodExpression;
import org.jboss.seam.core.Expressions.ValueExpression;
import org.jboss.seam.databinding.DataBinder;
import org.jboss.seam.databinding.DataSelector;
import org.jboss.seam.ejb.SeamInterceptor;
import org.jboss.seam.intercept.ClientSideInterceptor;
import org.jboss.seam.intercept.Interceptor;
import org.jboss.seam.intercept.JavaBeanInterceptor;
import org.jboss.seam.intercept.Proxy;
import org.jboss.seam.log.LogProvider;
import org.jboss.seam.log.Logging;
import org.jboss.seam.util.Conversions;
import org.jboss.seam.util.Naming;
import org.jboss.seam.util.ProxyFactory;
import org.jboss.seam.util.Reflections;
import org.jboss.seam.util.SortItem;
import org.jboss.seam.util.Sorter;
import org.jboss.seam.util.Conversions.PropertyValue;
import org.jboss.seam.web.Parameters;

/**
 * Metamodel class for component classes.
 * 
 * A Seam component is any class with a @Name annotation.
 *
 * @author Thomas Heute
 * @author Gavin King
 * 
 */
@Scope(ScopeType.APPLICATION)
public class Component extends Model
{
   public static final String PROPERTIES = "org.jboss.seam.properties";

   private static final LogProvider log = Logging.getLogProvider(Component.class);
   
   private ComponentType type;
   private String name;
   private ScopeType scope;
   private String jndiName;
   private boolean interceptionEnabled;
   private boolean startup;
   private String[] dependencies;
   private boolean synchronize;
   private long timeout;
   private boolean secure;

   private Set<Class> businessInterfaces;

   private Method destroyMethod;
   private Method createMethod;
   private Method unwrapMethod;
   private Method defaultRemoveMethod;
   
   //TODO: check the EJB3 spec, I think you
   //      are allowed to have multiple
   //      lifecycle methods on a bean!
   private Method preDestroyMethod;
   private Method postConstructMethod;
   private Method prePassivateMethod;
   private Method postActivateMethod;
   
   private Map<String, Method> removeMethods = new HashMap<String, Method>();
   private Set<Method> lifecycleMethods = new HashSet<Method>();
   private Set<Method> conversationManagementMethods = new HashSet<Method>();
   
   private List<BijectedAttribute<In>> inAttributes = new ArrayList<BijectedAttribute<In>>();
   private List<BijectedAttribute<Out>> outAttributes = new ArrayList<BijectedAttribute<Out>>();
   private List<BijectedAttribute> parameterSetters = new ArrayList<BijectedAttribute>();
   private List<BijectedAttribute> dataModelGetters = new ArrayList<BijectedAttribute>();
   private List<BijectedAttribute> pcAttributes = new ArrayList<BijectedAttribute>();
   private Map<String, BijectedAttribute> dataModelSelectionSetters = new HashMap<String, BijectedAttribute>();
   
   private List<Interceptor> interceptors = new ArrayList<Interceptor>();
   private List<Interceptor> clientSideInterceptors = new ArrayList<Interceptor>();

   private Map<Method, InitialValue> initializerSetters = new HashMap<Method, InitialValue>();
   private Map<Field, InitialValue> initializerFields = new HashMap<Field, InitialValue>();

   private List<Field> logFields = new ArrayList<Field>();
   private List<org.jboss.seam.log.Log> logInstances = new ArrayList<org.jboss.seam.log.Log>();
   
   private Collection<Namespace> imports = new ArrayList<Namespace>();
   private Namespace namespace;
   
   private boolean perNestedConversation;

   private Class<ProxyObject> factory;

   //only used for tests
   public Component(Class<?> clazz)
   {
      this( clazz, getComponentName(clazz) );
   }

   // only used for tests
   public Component(Class<?> clazz, String componentName)
   {
      this(clazz, componentName, Seam.getComponentScope(clazz), false, new String[0], null);
   }

   // only used for tests
   public Component(Class<?> clazz, Context applicationContext)
   {
      this( clazz, getComponentName(clazz), Seam.getComponentScope(clazz), false, new String[0], null, applicationContext );
   }

   public Component(Class<?> clazz, String componentName, ScopeType componentScope, boolean startup, String[] dependencies, String jndiName)
   {
      this(clazz, componentName, componentScope, startup, dependencies, jndiName, Contexts.getApplicationContext());
   }

   private Component(Class<?> beanClass, String componentName, ScopeType componentScope, boolean startup, String[] dependencies, String componentJndiName, Context applicationContext)
   {
      super(beanClass);
      
      name = componentName;
      scope = componentScope;
      this.startup = startup;
      this.dependencies = dependencies;
      type = Seam.getComponentType( getBeanClass() );
      interceptionEnabled = Seam.isInterceptionEnabled( getBeanClass() );
      perNestedConversation = hasAnnotation(getBeanClass(), PerNestedConversation.class);
      
      checkName();  
      checkNonabstract();
      
      initNamespace(componentName, applicationContext);
      initImports(applicationContext);
      initSynchronize();
      initStartup();
      initSecurity();

      checkScopeForComponentType();
      checkSynchronizedForComponentType();
      checkSerializableForComponentType();

      jndiName = componentJndiName == null ?
            getJndiName(applicationContext) : componentJndiName;

      log.info(
            "Component: " + getName() +
            ", scope: " + getScope() +
            ", type: " + getType() +
            ", class: " + getBeanClass().getName() +
            ( jndiName==null ? "" : ", JNDI: " + jndiName )
         );

      initMembers( getBeanClass(), applicationContext );
      checkDefaultRemoveMethod();

      businessInterfaces = getBusinessInterfaces( getBeanClass() );

      if ( interceptionEnabled )
      {
         initInterceptors();
      }

      initInitializers(applicationContext);
      
      registerConverterOrValidator(applicationContext);

   }

   private void checkName()
   {
      for ( char c: name.toCharArray() )
      {
         if ( !Character.isJavaIdentifierPart(c) && c!='.' )
         {
            throw new IllegalStateException("not a valid Seam component name: " + name);
         }
      }
   }

   private void checkNonabstract()
   {
      if ( getBeanClass().isInterface() )
      {
         throw new IllegalArgumentException("component class is an interface: " + name);
      }
      if ( Modifier.isAbstract( getBeanClass().getModifiers() ) )
      {
         throw new IllegalArgumentException("component class is abstract: " + name);
      }
   }

   private void initStartup()
   {
      if (startup)
      {
         if (scope!=SESSION && scope!=APPLICATION)
         {
            throw new IllegalArgumentException("@Startup only supported for SESSION or APPLICATION scoped components: " + name);
         }
         Startup annotation = getBeanClass().getAnnotation(Startup.class);
         if (dependencies.length == 0 && annotation != null)
         {
            dependencies = annotation.depends();
         }
      }
   }

   private void initSynchronize()
   {
      boolean hasAnnotation = getBeanClass().isAnnotationPresent(Synchronized.class); 
      
      // Technically, we don't need to synchronize page-scoped components if StateManager#isSavingStateInClient(FacesContext) is true 
      synchronize = ( scope==SESSION || scope==PAGE || hasAnnotation ) && type != ComponentType.STATEFUL_SESSION_BEAN;
            
      if (synchronize)
      {
         timeout = getBeanClass().isAnnotationPresent(Synchronized.class) ?
               getBeanClass().getAnnotation(Synchronized.class).timeout() :
               Synchronized.DEFAULT_TIMEOUT;
      }
      
      if (hasAnnotation && !interceptionEnabled)
      {
         log.warn("Interceptors are disabled for @Synchronized component - synchronization will be disabled for: " + name);
      }
      
      if (hasAnnotation && type == ComponentType.STATEFUL_SESSION_BEAN)
      {
         log.warn("Seam synchronization interceptor is disabled for @Synchronized @Stateful component - Seam synchronization will be disabled for: " + name);
      }
   }
   
   private void registerConverterOrValidator(Context applicationContext)
   {
      if (applicationContext!=null) //for unit tests!
      {
         Init init = (Init) applicationContext.get( Seam.getComponentName(Init.class) );
         if (init!=null)
         {
            if ( getBeanClass().isAnnotationPresent(Converter.class) )
            {
               if(!getBeanClass().isAnnotationPresent(BypassInterceptors.class))
                  throw new IllegalStateException("Converter " + getBeanClass().getName() 
                        + " must be annotated with @BypassInterceptors");
               
               Converter converter = getBeanClass().getAnnotation(Converter.class);
               if ( converter.forClass()!=void.class )
               {
                  init.getConvertersByClass().put( converter.forClass(), getName() );
               }
               String id = converter.id().equals("") ? getName() : converter.id();
               init.getConverters().put( id, getName() );
            }
            if ( getBeanClass().isAnnotationPresent(Validator.class) )
            {
               if(!getBeanClass().isAnnotationPresent(BypassInterceptors.class))
                  throw new IllegalStateException("Validator " + getBeanClass().getName() 
                        + " must be annotated with @BypassInterceptors");

               Validator validator = getBeanClass().getAnnotation(Validator.class);
               String id = validator.id().equals("") ? getName() : validator.id();
               init.getValidators().put( id, getName() );
            }
         }
      }
   }

   private void initNamespace(String componentName, Context applicationContext)
   {  
      if (applicationContext!=null) { //for unit tests!
         Init init = (Init) applicationContext.get(Seam.getComponentName(Init.class));
         if (init!=null) {
            this.namespace = init.initNamespaceForName(componentName, true);
         }
      }
   }
   
   private void initImports(Context applicationContext)
   {
      if (applicationContext!=null) //for unit tests!
      {
         Init init = (Init) applicationContext.get( Seam.getComponentName(Init.class) );
         if (init!=null)
         {
            if ( getBeanClass().isAnnotationPresent(Import.class) )
            {
               addImport( init, getBeanClass().getAnnotation(Import.class) );
            }
            
            Package pkg = getBeanClass().getPackage();
            if ( pkg!=null && pkg.isAnnotationPresent(Import.class) )
            {
               addImport( init, getBeanClass().getPackage().getAnnotation(Import.class) );
            }
         }
      }
   }

   private void addImport(Init init, Import imp)
   {
       for (String ns: imp.value()) {
           imports.add(init.initNamespaceForName(ns, false));
       }
   }

   private void checkScopeForComponentType()
   {
      if ( scope==STATELESS && (type==STATEFUL_SESSION_BEAN || type==ENTITY_BEAN) )
      {
         throw new IllegalArgumentException("Only stateless session beans and Java beans may be bound to the STATELESS context: " + name);
      }
      if ( scope==PAGE && type==STATEFUL_SESSION_BEAN )
      {
         throw new IllegalArgumentException("Stateful session beans may not be bound to the PAGE context: " + name);
      }
      if ( scope==APPLICATION && type==STATEFUL_SESSION_BEAN )
      {
         log.warn("Stateful session beans was bound to the APPLICATION context - note that it is not safe to make concurrent calls to the bean: " + name);
      }
      if ( scope!=STATELESS && type==MESSAGE_DRIVEN_BEAN )
      {
         throw new IllegalArgumentException("Message-driven beans must be bound to STATELESS context: " + name);
      }      
      if ( scope!=STATELESS && type==ComponentType.STATELESS_SESSION_BEAN )
      {
          log.warn("Stateless session beans should only be bound to the STATELESS context:" + name);
      }      
   }
   
   protected void checkSynchronizedForComponentType()
   {
      if (scope==STATELESS && synchronize)
      {
         throw new IllegalArgumentException("@Synchronized not meaningful for stateless components: " + name);
      }
   }
   
   private void checkSerializableForComponentType()
   {
      boolean serializableScope = scope==PAGE || scope==SESSION || scope==CONVERSATION;
      boolean serializableType = type==JAVA_BEAN || type==ENTITY_BEAN;
      if ( serializableType && serializableScope && !Serializable.class.isAssignableFrom( getBeanClass() ) )
      {
         log.warn("Component class should be serializable: " + name);
      }
   }

   private String getJndiName(Context applicationContext)
   {
      if ( getBeanClass().isAnnotationPresent(JndiName.class) )
      {
         return getBeanClass().getAnnotation(JndiName.class).value();
      }
      else
      {
         switch (type) {
            case ENTITY_BEAN:
            case JAVA_BEAN:
               return null;
            default:
               if (applicationContext==null) return null; //TODO: Yew!!!
               String jndiPattern = Init.instance().getJndiPattern();
               if (jndiPattern==null)
               {
                  throw new IllegalArgumentException("You must specify org.jboss.seam.core.init.jndiPattern or use @JndiName: " + name);
               }
               return jndiPattern.replace( "#{ejbName}", Seam.getEjbName(getBeanClass()) );
         }
      }
   }

   private void initInitializers(Context applicationContext)
   {
      if (applicationContext==null) return; //TODO: yew!!!!!
      Map<String, Conversions.PropertyValue> properties = (Map<String, Conversions.PropertyValue>) applicationContext.get(PROPERTIES);
      if (properties==null) return; //TODO: yew!!!!!


      for ( Map.Entry<String, Conversions.PropertyValue> me: properties.entrySet() )
      {
         String key = me.getKey();
         Conversions.PropertyValue propertyValue = me.getValue();

         if ( key.startsWith(name) && key.charAt( name.length() )=='.' )
         {
            if ( log.isDebugEnabled() ) log.debug( key + "=" + propertyValue );

            /*if ( type==ENTITY_BEAN )
            {
               throw new IllegalArgumentException("can not configure entity beans: " + name);
            }*/

            String propertyName = key.substring( name.length()+1, key.length() );
            Method setterMethod = null;
            try { 
                setterMethod = Reflections.getSetterMethod(getBeanClass(), propertyName);
            } catch (IllegalArgumentException e) {}
            if (setterMethod!=null)
            {
               if ( !setterMethod.isAccessible() ) setterMethod.setAccessible(true);
               Class parameterClass = setterMethod.getParameterTypes()[0];
               Type parameterType = setterMethod.getGenericParameterTypes()[0];
               initializerSetters.put( setterMethod, getInitialValueHonoringExceptions(propertyName, propertyValue, parameterClass, parameterType) );
            }
            else
            {
               Field field = Reflections.getField(getBeanClass(), propertyName);
               if ( !field.isAccessible() ) field.setAccessible(true);
               initializerFields.put( field, getInitialValue(propertyValue, field.getType(), field.getGenericType()) );
            }
        }

      }
   }

   private InitialValue getInitialValueHonoringExceptions(String propertyName, Conversions.PropertyValue propertyValue, Class parameterClass, Type parameterType)
   {
      // the org.jboss.seam.core.init component cannot accomodate EL expressions because of startup order
      // note that org.jboss.seam.core.init.jndiPattern looks like an EL expression but is not one!
      if ("org.jboss.seam.core.init".equals(getName()))
      {
         return new ConstantInitialValue(propertyValue, parameterClass, parameterType);
      }
      else
      {
         return getInitialValue(propertyValue, parameterClass, parameterType);
      }
   }

   private static InitialValue getInitialValue(Conversions.PropertyValue propertyValue, Class parameterClass, Type parameterType)
   {
      if ( parameterClass.equals(ValueExpression.class) ||
           parameterClass.equals(MethodExpression.class) ||
            propertyValue.isExpression() )
      {
         return new ELInitialValue(propertyValue, parameterClass, parameterType);
      }
      else if ( propertyValue.isMultiValued() )
      {
          if (Set.class.isAssignableFrom(parameterClass)) {
             return new SetInitialValue(propertyValue, parameterClass, parameterType);
          } else {
             return new ListInitialValue(propertyValue, parameterClass, parameterType);
          }
      }
      else if ( propertyValue.isAssociativeValued() )
      {
         return new MapInitialValue(propertyValue, parameterClass, parameterType);
      }
      else
      {
         return new ConstantInitialValue(propertyValue, parameterClass, parameterType);
      }
   }

   private void initMembers(Class<?> clazz, Context applicationContext)
   {
      Map<Method, Annotation> selectionSetters = new HashMap<Method, Annotation>();
      Map<Field, Annotation> selectionFields = new HashMap<Field, Annotation>();
      Set<String> dataModelNames = new HashSet<String>();

      for ( ; clazz!=Object.class; clazz = clazz.getSuperclass() )
      {
         for ( Method method: clazz.getDeclaredMethods() )
         {
            scanMethod(applicationContext, selectionSetters, dataModelNames, method);
         }

         for ( Field field: clazz.getDeclaredFields() )
         {
            scanField(selectionFields, dataModelNames, field);
         }
      }
      
      final boolean hasMultipleDataModels = dataModelGetters.size() > 1;
      String defaultDataModelName = null;
      if ( !hasMultipleDataModels )
      {
         if ( !dataModelGetters.isEmpty() )
         {
            defaultDataModelName = dataModelGetters.get(0).getName();
         }
      }

      for ( Map.Entry<Method, Annotation> annotatedMethod: selectionSetters.entrySet() )
      {
         Method method = annotatedMethod.getKey();
         Annotation ann = annotatedMethod.getValue();
         String name = getDataModelSelectionName(dataModelNames, hasMultipleDataModels, defaultDataModelName, ann);
         Object existing = dataModelSelectionSetters.put( name, new BijectedMethod(name, method, ann) );
         if (existing!=null)
         {
            throw new IllegalStateException("Multiple @DataModelSelection setters for: " + name);
         }
      }

      for ( Map.Entry<Field, Annotation> annotatedField: selectionFields.entrySet() )
      {
         Field field = annotatedField.getKey();
         Annotation ann = annotatedField.getValue();
         String name = getDataModelSelectionName(dataModelNames, hasMultipleDataModels, defaultDataModelName, ann);
         Object existing = dataModelSelectionSetters.put( name, new BijectedField(name, field, ann) );
         if (existing!=null)
         {
            throw new IllegalStateException("Multiple @DataModelSelection fields for: " + name);
         }
      }

   }

   private void checkDefaultRemoveMethod()
   {
      if (type==STATEFUL_SESSION_BEAN)
      {
         if ( destroyMethod!=null && destroyMethod.isAnnotationPresent(REMOVE) ) //TODO: @Remove is not declared @Inherited, but does the EJB container emulate that?
         {
            //we don't need to worry about default remove methods
            defaultRemoveMethod = null;
         }
         else
         {
            //check that we have a default remove method
            if ( defaultRemoveMethod==null )
            {
               throw new IllegalArgumentException("Stateful session bean component must have a method with no parameters marked @Remove: " + name);
            }
            
            //check that it is unique
            boolean found = false;
            for ( Method remove: removeMethods.values() )
            {
               if ( remove.getParameterTypes().length==0 )
               {
                  if (found)
                  {
                     throw new IllegalStateException("Duplicate default @Remove method for component:" + name);
                  }
                  found = true;
               }
            }
         }
      }
   }

   private void scanMethod(Context applicationContext, Map<Method, Annotation> selectionSetters, Set<String> dataModelNames, Method method)
   {
      if ( method.isAnnotationPresent(Destroy.class) )
      {
         /*if ( method.getParameterTypes().length>0 ) and it doesn't take a Component parameter
         {
            throw new IllegalStateException("@Destroy methods may not have parameters: " + name);
         }*/
         if (type!=JAVA_BEAN && type!=STATEFUL_SESSION_BEAN)
         {
            throw new IllegalArgumentException("Only JavaBeans and stateful session beans support @Destroy methods: " + name);
         }
         if ( destroyMethod!=null && !destroyMethod.getName().equals( method.getName() ) )
         {
            throw new IllegalStateException("component has two @Destroy methods: " + name);
         }
         
         if ( destroyMethod==null ) //ie. ignore the one on the superclass
         {
            destroyMethod = method;
            lifecycleMethods.add(method);
         }
      }
      
      if ( method.isAnnotationPresent(REMOVE) )
      {
         removeMethods.put( method.getName(), method );
         if ( method.getParameterTypes().length==0 )
         {
            defaultRemoveMethod = method;
            lifecycleMethods.add(method);
         }
      }
      
      if ( method.isAnnotationPresent(Create.class) )
      {
         /*if ( method.getParameterTypes().length>0 ) and it doesn't take a Component parameter
         {
            throw new IllegalStateException("@Create methods may not have parameters: " + name);
         }*/
         if (type!=JAVA_BEAN && type!=STATEFUL_SESSION_BEAN)
         {
            throw new IllegalArgumentException("Only JavaBeans and stateful session beans support @Create methods: " + name);
         }
         if ( createMethod!=null && !createMethod.getName().equals( method.getName() ) )
         {
            throw new IllegalStateException("component has two @Create methods: " + name);
         }
         if (createMethod==null)
         {
            createMethod = method;
            lifecycleMethods.add(method);
         }
      }
      
      if ( method.isAnnotationPresent(In.class) )
      {
         In in = method.getAnnotation(In.class);
         String name = toName( in.value(), method );
         inAttributes.add( new BijectedMethod(name, method, in) );
      }
      
      if ( method.isAnnotationPresent(Out.class) )
      {
         Out out = method.getAnnotation(Out.class);
         String name = toName( out.value(), method );
         outAttributes.add( new BijectedMethod(name, method, out) );
         
         //can't use Init.instance() here because of unit tests
         Init init = (Init) applicationContext.get(Seam.getComponentName(Init.class));
         init.initNamespaceForName(name, true);
      }
      
      if ( method.isAnnotationPresent(Unwrap.class) )
      {
         if ( unwrapMethod!=null && !unwrapMethod.getName().equals( method.getName() )  )
         {
            throw new IllegalStateException("component has two @Unwrap methods: " + name);
         }
         if (unwrapMethod==null )
         {
            unwrapMethod = method;
         }
      }
      
      if ( method.isAnnotationPresent(DataModel.class) ) //TODO: generalize
      {
         checkDataModelScope( method.getAnnotation(DataModel.class) );
      }
      
      if ( method.isAnnotationPresent(org.jboss.seam.annotations.Factory.class) )
      {
         //can't use Init.instance() here because of unit tests
         Init init = (Init) applicationContext.get(Seam.getComponentName(Init.class));
         String contextVariable = toName( method.getAnnotation(org.jboss.seam.annotations.Factory.class).value(), method );
         
         if ( contextVariable.equals(name) )
         {
            throw new IllegalStateException("@Factory method can not share name with its containing component: " + contextVariable);
         }
         
         init.addFactoryMethod(contextVariable, method, this);
         if ( method.getAnnotation(org.jboss.seam.annotations.Factory.class).autoCreate() )
         {
            init.addAutocreateVariable(contextVariable);
         }
      }
      
      if ( method.isAnnotationPresent(Observer.class) )
      {
         //can't use Init.instance() here because of unit tests
         Init init = (Init) applicationContext.get(Seam.getComponentName(Init.class));
          
         Observer observer = method.getAnnotation(Observer.class);
         for ( String eventType : observer.value() )
         {
            if ( eventType.length()==0 ) eventType = method.getName(); //TODO: new defaulting rule to map @Observer onFooEvent() -> event type "fooEvent"
            init.addObserverMethod( eventType, method, this, observer.create() );
         }
      }
      
      if ( method.isAnnotationPresent(RequestParameter.class) )
      {
         RequestParameter rp = method.getAnnotation(RequestParameter.class);
         String name = toName( rp.value(), method );
         parameterSetters.add( new BijectedMethod(name, method, rp) );
      }
      
      // TODO: verify that these life-cycle methods have no arguments!
      if ( method.isAnnotationPresent(PRE_PASSIVATE) )
      {
         prePassivateMethod = method;
         lifecycleMethods.add(method);
      }
      
      if ( method.isAnnotationPresent(POST_ACTIVATE) )
      {
         postActivateMethod = method;
         lifecycleMethods.add(method);
      }
      
      if ( method.isAnnotationPresent(POST_CONSTRUCT) )
      {
         postConstructMethod = method;
         lifecycleMethods.add(method);
      }
      
      if ( method.isAnnotationPresent(PRE_DESTROY) )
      {
         preDestroyMethod = method;
         lifecycleMethods.add(method);
      }
      
      if ( method.isAnnotationPresent(PERSISTENCE_CONTEXT) )
      {
         checkPersistenceContextForComponentType();
         pcAttributes.add( new BijectedMethod( toName(null, method), method, null ) );
      }
      
      if ( method.isAnnotationPresent(Begin.class) || 
           method.isAnnotationPresent(End.class) )
      {
         conversationManagementMethods.add(method);
      }
      
      for ( Annotation ann: method.getAnnotations() )
      {
         if ( ann.annotationType().isAnnotationPresent(DataBinderClass.class) )
         {
            String name = toName( createWrapper(ann).getVariableName(ann), method );
            dataModelGetters.add( new BijectedProperty(name, method, ann) );
            dataModelNames.add(name);
         }
         if ( ann.annotationType().isAnnotationPresent(DataSelectorClass.class) )
         {
            selectionSetters.put(method, ann);
         }
      }

      if ( !method.isAccessible() )
      {
         method.setAccessible(true);
      }
   }

   private void scanField(Map<Field, Annotation> selectionFields, Set<String> dataModelNames, Field field)
   {
      if ( !field.isAccessible() )
      {
         field.setAccessible(true);
      }

      if ( field.isAnnotationPresent(In.class) )
      {
         In in = field.getAnnotation(In.class);
         String name = toName( in.value(), field );
         inAttributes.add( new BijectedField(name, field, in) );
      }
      
      if ( field.isAnnotationPresent(Out.class) )
      {
         Out out = field.getAnnotation(Out.class);
         String name = toName( out.value(), field );
         outAttributes.add(new BijectedField(name, field, out) );
      }
      
      if ( field.isAnnotationPresent(DataModel.class) ) //TODO: generalize
      {
         checkDataModelScope( field.getAnnotation(DataModel.class) );
      }
      
      if ( field.isAnnotationPresent(RequestParameter.class) )
      {
         RequestParameter rp = field.getAnnotation(RequestParameter.class);
         String name = toName( rp.value(), field );
         parameterSetters.add( new BijectedField(name, field, rp) );
      }
      
      if ( field.isAnnotationPresent(org.jboss.seam.annotations.Logger.class) )
      {
         String category = field.getAnnotation(org.jboss.seam.annotations.Logger.class).value();
         org.jboss.seam.log.Log logInstance;
         if ( "".equals( category ) )
         {
            logInstance = org.jboss.seam.log.Logging.getLog(getBeanClass());
         }
         else
         {
            logInstance = org.jboss.seam.log.Logging.getLog(category);
         }
         if ( Modifier.isStatic( field.getModifiers() ) )
         {
            Reflections.setAndWrap(field, null, logInstance);
         }
         else
         {
            logFields.add(field);
            logInstances.add(logInstance);
         }
      }
      
      if ( field.isAnnotationPresent(PERSISTENCE_CONTEXT) )
      {
         checkPersistenceContextForComponentType();
         pcAttributes.add( new BijectedField( toName(null, field), field, null ) );
      }
      
      for ( Annotation ann: field.getAnnotations() )
      {
         if ( ann.annotationType().isAnnotationPresent(DataBinderClass.class) )
         {
            String name = toName( createWrapper(ann).getVariableName(ann), field );
            dataModelGetters.add( new BijectedField(name, field, ann) );
            dataModelNames.add(name);
         }
         if ( ann.annotationType().isAnnotationPresent(DataSelectorClass.class) )
         {
            selectionFields.put(field, ann);
         }
      }
      
   }

   protected void checkPersistenceContextForComponentType()
   {
      if ( !type.isSessionBean() && type!=MESSAGE_DRIVEN_BEAN )
      {
         throw new IllegalArgumentException("@PersistenceContext may only be used on session bean or message driven bean components: " + name);
      }
   }

   private String getDataModelSelectionName(Set<String> dataModelNames, boolean hasMultipleDataModels, String defaultDataModelName, Annotation ann)
   {
      String name = createUnwrapper(ann).getVariableName(ann);
      if ( name.length() == 0 )
      {
         if ( hasMultipleDataModels )
         {
            throw new IllegalStateException( "Missing value() for @DataModelSelection with multiple @DataModels" );
         }
         if ( defaultDataModelName==null )
         {
            throw new IllegalStateException("No @DataModel for @DataModelSelection: " + name);
         }
         return defaultDataModelName;
      }
      else
      {
         if ( !dataModelNames.contains(name) )
         {
            throw new IllegalStateException("No @DataModel for @DataModelSelection: " + name);
         }
         return name;
      }
   }

   private void checkDataModelScope(DataModel dataModel) {
      ScopeType dataModelScope = dataModel.scope();
      if ( dataModelScope!=PAGE && dataModelScope!=UNSPECIFIED )
      {
         throw new IllegalArgumentException("@DataModel scope must be ScopeType.UNSPECIFIED or ScopeType.PAGE: " + name);
      }
   }

   private void initInterceptors()
   {
      initDefaultInterceptors();

      for ( Annotation annotation: getBeanClass().getAnnotations() )
      { 
         if ( annotation.annotationType().isAnnotationPresent(INTERCEPTORS) )
         {
            Class[] classes = value( annotation.annotationType().getAnnotation(INTERCEPTORS) );
            addInterceptor( new Interceptor(classes, annotation, this) );
         }
         if ( annotation.annotationType().isAnnotationPresent(Interceptors.class) )
         {
            Class[] classes = annotation.annotationType().getAnnotation(Interceptors.class).value();
            addInterceptor( new Interceptor(classes, annotation, this) );
         }
      }

      newSort(interceptors);
      newSort(clientSideInterceptors);
      
      if ( log.isDebugEnabled() ) log.debug("interceptor stack: " + interceptors);
   }

   public void addInterceptor(Object interceptorInstance)
   {
      try
      {
         addInterceptor(new Interceptor(interceptorInstance, this));
      }
      catch (NoClassDefFoundError e)
      {
         log.debug("Unable to load interceptor " + interceptorInstance.getClass(), e);
      }
      catch (TypeNotPresentException e) 
      {
         log.debug("Unable to load interceptor " + interceptorInstance.getClass(), e);
      }
      catch (Exception e)
      {
         throw new IllegalArgumentException("Unable to load interceptor " + interceptorInstance.getClass(), e);
      }
   }
   
   public void addInterceptor(Interceptor interceptor)
   {
      if (interceptor.isInterceptorEnabled())
      {
         if (interceptor.getType()==InterceptorType.SERVER) 
         {
            interceptors.add(interceptor);
         }
         else 
         {
            clientSideInterceptors.add(interceptor);
         }
      }
   }

   private List<Interceptor> newSort(List<Interceptor> list)
   {
      List<SortItem<Interceptor>> siList = new ArrayList<SortItem<Interceptor>>();
      Map<Class<?>,SortItem<Interceptor>> ht = 
          new HashMap<Class<?>,SortItem<Interceptor>>();

      for (Interceptor i: list) {
         SortItem<Interceptor> si = new SortItem<Interceptor>(i);
         siList.add(si);
         ht.put(i.getUserInterceptorClass(), si);
      }

      for (SortItem<Interceptor> si : siList) {
          Class<?> clazz = si.getObj().getUserInterceptorClass();
          org.jboss.seam.annotations.intercept.Interceptor interceptorAnn = 
              seamInterceptor(clazz);
          
          if (interceptorAnn != null) {
              for (Class<?> cl : Arrays.asList(interceptorAnn.around())) {
                  if (!isCompatibleInterceptor(interceptorAnn, seamInterceptor(cl))) {
                      log.warn("Interceptor " + clazz +  
                               " has different type than around interceptor " + cl);
                  }

                  si.addAround(ht.get(cl));
              }
            
            for (Class<?> cl : Arrays.asList( interceptorAnn.within())) {
                if (!isCompatibleInterceptor(interceptorAnn, seamInterceptor(cl))) {                
                    log.warn("Interceptor " + clazz +  
                            " has different type than within interceptor " + cl);
                }
                
                si.addWithin(ht.get(cl));
            }
         }
      }

      Sorter<Interceptor> sList = new Sorter<Interceptor>();
      siList = sList.sort(siList);

      list.clear();
      for (SortItem<Interceptor> si : siList) {
         list.add(si.getObj());
      }
      return list ;
   }
   
   private org.jboss.seam.annotations.intercept.Interceptor seamInterceptor(Class<?> clazz) {
       return clazz.getAnnotation(org.jboss.seam.annotations.intercept.Interceptor.class);
   }

   private boolean isCompatibleInterceptor(
            org.jboss.seam.annotations.intercept.Interceptor anno1,
            org.jboss.seam.annotations.intercept.Interceptor anno2) {

        if (anno1==null || anno2==null) {
            return true;
        }
        
        if (anno1.type()==InterceptorType.CLIENT && anno2.type()==InterceptorType.SERVER) {
            return false;
        } 
        
        if (anno2.type()==InterceptorType.CLIENT && anno2.type()==InterceptorType.SERVER) {
            return false;
        }
         
        return true;
    }

   private void initDefaultInterceptors()
   {
      List<String> interceptors;
      if (Contexts.isApplicationContextActive())
      {
         interceptors = Init.instance().getInterceptors();
      }
      else
      {
         // For unit tests
         interceptors = Init.DEFAULT_INTERCEPTORS;
      }
      for (String interceptorName : interceptors)
      {
         Object interceptorInstance = null;
         try
         {
            Class<?> clazz = Reflections.classForName(interceptorName);
            interceptorInstance = clazz.newInstance();
            
         }
         catch (NoClassDefFoundError e)
         {
            log.debug("Unable to load interceptor " + interceptorName, e);
         }
         catch (TypeNotPresentException e) 
         {
            log.debug("Unable to load interceptor " + interceptorName, e);
         }
         catch (Exception e)
         {
            throw new IllegalArgumentException("Unable to load interceptor " + interceptorName, e);
         }
         addInterceptor(interceptorInstance);
         
      }
   }
   
   private void initSecurity()
   {
      if ( beanClassHasAnnotation(Restrict.class) )
      {
         secure = true;
         return;
      }
      
      for (Annotation annotation : getBeanClass().getAnnotations())
      {
         if (annotation.annotationType().isAnnotationPresent(RoleCheck.class))
         {
            secure = true;
            return;
         }
      }
      
      for (Method method : getBeanClass().getMethods())
      {
         for (Annotation annotation : method.getAnnotations())
         {
            if (annotation.annotationType().isAnnotationPresent(PermissionCheck.class) ||
                  annotation.annotationType().isAnnotationPresent(RoleCheck.class))
            {
               secure = true;
               return;
            }
         }   
         
         for (Annotation[] annotations : method.getParameterAnnotations())
         {
            for (Annotation annotation : annotations)
            {
               if (annotation.annotationType().isAnnotationPresent(PermissionCheck.class))
               {
                  secure = true;
                  return;
               }
               
            }
         }
      }
   }

   private static boolean hasAnnotation(Class clazz, Class annotationType)
   {
      if ( clazz.isAnnotationPresent(annotationType) )
      {
         return true;
      }
      else
      {
         for ( Method method: clazz.getMethods() )
         {
            if ( method.isAnnotationPresent(annotationType) )
            {
               return true;
            }
         }
         return false;
      }
   }
   
   private static boolean hasAnnotation(Class clazz, String annotationName)
   {
      for (Annotation a : clazz.getAnnotations())
      {
         if (a.annotationType().getName().equals(annotationName)) return true;          
      }
      
      for ( Method method : clazz.getMethods() )
      {
         for ( Annotation a : method.getAnnotations() )
         {
            if (a.annotationType().getName().equals(annotationName)) return true;
         }
      }
      
      return false;
   }

   public boolean beanClassHasAnnotation(Class annotationType)
   {
      return hasAnnotation( getBeanClass(), annotationType );
   }
   
   public boolean beanClassHasAnnotation(String annotationName)
   {
      return hasAnnotation( getBeanClass(), annotationName );
   }

   public boolean businessInterfaceHasAnnotation(Class annotationType)
   {
      for (Class businessInterface: getBusinessInterfaces() )
      {
         if ( hasAnnotation(businessInterface, annotationType) )
         {
            return true;
         }
      }
      return false;
   }

   public String getName()
   {
      return name;
   }

   public ComponentType getType()
   {
      return type;
   }

   public ScopeType getScope()
   {
      return scope;
   }

   public List<Interceptor> getInterceptors(InterceptorType type)
   {
      switch(type)
      {
         case SERVER: return interceptors;
         case CLIENT: return clientSideInterceptors;
         case ANY:
            List<Interceptor> all = new ArrayList<Interceptor>();
            all.addAll(clientSideInterceptors);
            all.addAll(interceptors);
            return all;
         default: throw new IllegalArgumentException("no interceptor type specified");
      }
   }

   public List<Object> createUserInterceptors(InterceptorType type)
   {
      List<Interceptor> interceptors = getInterceptors(type);
      List<Object> result = new ArrayList<Object>( interceptors.size() );
      for (Interceptor interceptor: interceptors)
      {
         result.add( interceptor.createUserInterceptor() );
      }
      return result;
   }

   /**
    * For use with Seam debug page.
    *
    * @return the server-side interceptor stack
    */
   public List<Interceptor> getServerSideInterceptors()
   {
      return getInterceptors(InterceptorType.SERVER);
   }

   /**
    * For use with Seam debug page.
    *
    * @return the client-side interceptor stack
    */
   public List<Interceptor> getClientSideInterceptors()
   {
      return getInterceptors(InterceptorType.CLIENT);
   }

   public Method getDestroyMethod()
   {
      return destroyMethod;
   }

   public Collection<Method> getRemoveMethods()
   {
      return removeMethods.values();
   }

   public Method getRemoveMethod(String name)
   {
      return removeMethods.get(name);
   }

   public boolean hasPreDestroyMethod()
   {
      return preDestroyMethod!=null;
   }

   public boolean hasPostConstructMethod()
   {
      return postConstructMethod!=null;
   }

   public boolean hasPrePassivateMethod()
   {
      return prePassivateMethod!=null;
   }

   public boolean hasPostActivateMethod()
   {
      return postActivateMethod!=null;
   }

   public boolean hasDestroyMethod()
   {
      return destroyMethod!=null;
   }

   public boolean hasCreateMethod()
   {
      return createMethod!=null;
   }

   public Method getCreateMethod()
   {
      return createMethod;
   }

   public boolean hasUnwrapMethod()
   {
      return unwrapMethod!=null;
   }

   public Method getUnwrapMethod()
   {
      return unwrapMethod;
   }

   public List<BijectedAttribute<Out>> getOutAttributes()
   {
      return outAttributes;
   }

   public List<BijectedAttribute<In>> getInAttributes()
   {
      return inAttributes;
   }

    public boolean needsInjection() 
    {
        return 
            !getInAttributes().isEmpty() ||
            !dataModelSelectionSetters.isEmpty() ||
            !parameterSetters.isEmpty();
    }

    public boolean needsOutjection() 
    {
        return 
            !getOutAttributes().isEmpty() ||
            !dataModelGetters.isEmpty();
    }

    protected Object instantiate() throws Exception
    {
        switch(type) {
           case JAVA_BEAN:
              return instantiateJavaBean();
           case ENTITY_BEAN:
              return instantiateEntityBean();
           case STATELESS_SESSION_BEAN:
           case STATEFUL_SESSION_BEAN:
           case SINGLETON_SESSION_BEAN:
              return instantiateSessionBean();
           case MESSAGE_DRIVEN_BEAN:
              throw new UnsupportedOperationException("Message-driven beans may not be called: " + name);
           default:
              throw new IllegalStateException();
        }
    }

    protected void postConstruct(Object bean) throws Exception
    {
        switch(type) {
           case JAVA_BEAN:
              postConstructJavaBean(bean);
              break;
           case ENTITY_BEAN:
              postConstructEntityBean(bean);
              break;
           case STATELESS_SESSION_BEAN:
           case STATEFUL_SESSION_BEAN:
           case SINGLETON_SESSION_BEAN:
              postConstructSessionBean(bean);
              break;
           case MESSAGE_DRIVEN_BEAN:
              throw new UnsupportedOperationException("Message-driven beans may not be called: " + name);
           default:
              throw new IllegalStateException();
        }
    }
    

   protected Object instantiateSessionBean() 
       throws Exception, 
              NamingException
   {
      Component old = SeamInterceptor.COMPONENT.get();
      SeamInterceptor.COMPONENT.set(this);
      try {
         Object bean = Naming.getInitialContext().lookup(jndiName);
         return wrap(bean, new ClientSideInterceptor(bean, this));
      } finally {
         SeamInterceptor.COMPONENT.set(old);
      }
   }

   
   protected void postConstructSessionBean(Object bean) 
       throws Exception, 
              NamingException
   {
        // ...
   }
   
   
   protected Object instantiateEntityBean() throws Exception
   {
      Constructor constructor = getBeanClass().getConstructor(new Class[0]);
      boolean accessible = constructor.isAccessible();
      if (Modifier.isProtected(constructor.getModifiers()))
      {
         constructor.setAccessible(true);
      }
      Object bean = getBeanClass().newInstance();
      constructor.setAccessible(accessible);
      return bean;
   }
   
   
   protected void postConstructEntityBean(Object bean) 
       throws Exception
   {
      initialize(bean);
   }

   protected Object instantiateJavaBean() throws Exception
   {
      Object bean = getBeanClass().newInstance();
     
      if (interceptionEnabled) {
         JavaBeanInterceptor interceptor = new JavaBeanInterceptor(bean, this);
         bean = wrap(bean, interceptor);
      }
      
      return bean;
   }
   
   
   protected void postConstructJavaBean(Object bean) 
       throws Exception
   {
      if (!interceptionEnabled) {
         initialize(bean);
         callPostConstructMethod(bean);
      } else {
         if (bean instanceof Proxy) {
             Proxy proxy = (Proxy) bean;
             JavaBeanInterceptor interceptor = (JavaBeanInterceptor) proxy.writeReplace();

             interceptor.postConstruct();
         }
      }
   }
   
   public void destroy(Object bean)
   {
      try
      {
         callDestroyMethod(bean);
      }
      catch (Exception e)
      {
         log.warn("Exception calling component @Destroy method: " + name, e);
      }
      if ( getType()==STATEFUL_SESSION_BEAN )
      {
         try
         {
            callDefaultRemoveMethod(bean);
         }
         catch (Exception e)
         {
            log.warn("Exception calling stateful session bean default @Remove method: " + name, e);
         }
      }
      else if ( getType()==JAVA_BEAN )
      {
         try
         {
            callPreDestroyMethod(bean);
         }
         catch (Exception e)
         {
            log.warn("Exception calling JavaBean @PreDestroy method: " + name, e);
         }
      }
   }

   /**
    * Wrap a Javassist interceptor around an instance of the component
    */
   public Object wrap(Object bean, MethodHandler interceptor) throws Exception
   {
      ProxyObject proxy = getProxyFactory().newInstance();
      proxy.setHandler(interceptor);
      return proxy;
   }

   private synchronized Class<ProxyObject> getProxyFactory()
   {
      if (factory==null)
      {
         factory = createProxyFactory( getType(), getBeanClass(), getBusinessInterfaces() );
      }
      return factory;
   }

   public void initialize(Object bean) throws Exception
   {
      if ( log.isTraceEnabled() ) log.trace("initializing new instance of: " + name);

      injectLog(bean);

      for ( Map.Entry<Method, InitialValue> me: initializerSetters.entrySet() )
      {
         Method method = me.getKey();
         Object initialValue = me.getValue().getValue( method.getParameterTypes()[0] );
         setPropertyValue(bean, method, method.getName(), initialValue );
      }
      for ( Map.Entry<Field, InitialValue> me: initializerFields.entrySet() )
      {
         Field field = me.getKey();
         Object initialValue = me.getValue().getValue( field.getType() );
         setFieldValue(bean, field, field.getName(), initialValue );
      }

      if ( log.isTraceEnabled() ) log.trace("done initializing: " + name);
   }

   /**
    * Inject context variable values into @In attributes
    * of a component instance.
    *
    * @param bean a Seam component instance
    * @param enforceRequired should we enforce required=true?
    */
   public void inject(Object bean, boolean enforceRequired)
   {
      if ( log.isTraceEnabled() )
      {
         log.trace("injecting dependencies of: " + getName());
      }
      //injectLog(bean);
      injectAttributes(bean, enforceRequired);
      injectDataModelSelections(bean);
      injectParameters(bean);
   }

   /**
    * Null out any @In attributes of a component instance.
    *
    * @param bean a Seam component instance
    */
   public void disinject(Object bean)
   {
      if ( log.isTraceEnabled() )
      {
         log.trace("disinjecting dependencies of: " + getName());
      }
      disinjectAttributes(bean);
   }

   private void injectLog(Object bean)
   {
      for (int i=0; i<logFields.size(); i++)
      {
         setFieldValue( bean, logFields.get(i), "log", logInstances.get(i) );
      }
   }

   private void injectParameters(Object bean)
   {
      Parameters params = Parameters.instance();
      if (params!=null) //check for unit tests
      {
         Map<String, String[]> requestParameters = params.getRequestParameters();
         for (BijectedAttribute setter: parameterSetters)
         {
            Object convertedValue = params.convertMultiValueRequestParameter(requestParameters, setter.getName(), setter.getType());
            setter.set(bean, convertedValue);
         }
      }
   }

   /**
    * Outject context variable values from @Out attributes
    * of a component instance.
    *
    * @param bean a Seam component instance
    * @param enforceRequired should we enforce required=true?
    */
   public void outject(Object bean, boolean enforceRequired)
   {
      if ( log.isTraceEnabled() )
      {
         log.trace("outjecting dependencies of: " + getName());
      }
      outjectAttributes(bean, enforceRequired);
      outjectDataModels(bean);
   }

   private void injectDataModelSelections(Object bean)
   {
      for ( BijectedAttribute dataModelGetter: dataModelGetters )
      {
         injectDataModelSelection(bean, dataModelGetter);
      }
   }

   private void injectDataModelSelection(Object bean, BijectedAttribute dataModelGetter)
   {
      DataBinder wrapper = createWrapper( dataModelGetter.getAnnotation() );
      String name = dataModelGetter.getName();
      Annotation dataModelAnn = dataModelGetter.getAnnotation();
      ScopeType scope = wrapper.getVariableScope(dataModelAnn);
      
      Object dataModel = getOutScope(scope, this).getContext().get(name);
      if ( dataModel!=null )
      {
         if ( PAGE.equals(scope) )
         {
            dataModelGetter.set(bean, wrapper.getWrappedData(dataModelAnn, dataModel));
         }
      
         Object selectedIndex = wrapper.getSelection(dataModelAnn, dataModel);
      
         if ( log.isDebugEnabled() ) log.debug( "selected row: " + selectedIndex );
      
         if ( selectedIndex!=null )
         {
            BijectedAttribute setter = dataModelSelectionSetters.get(name);
            if (setter != null)
            {
               Annotation dataModelSelectionAnn = setter.getAnnotation();
               Object selection = createUnwrapper(dataModelSelectionAnn).getSelection(dataModelSelectionAnn, dataModel);
               setter.set(bean, selection);
            }
         }
      
      }
   }

   private void outjectDataModels(Object bean)
   {
      for ( BijectedAttribute dataModelGetter: dataModelGetters )
      {
         outjectDataModel(bean, dataModelGetter);
      }
   }

   private void outjectDataModel(Object bean, BijectedAttribute dataModelGetter)
   {
      
      DataBinder wrapper = createWrapper( dataModelGetter.getAnnotation() );
      Object list = dataModelGetter.get(bean);
      String name = dataModelGetter.getName();
      Annotation dataModelAnn = dataModelGetter.getAnnotation();
      ScopeType scope = wrapper.getVariableScope(dataModelAnn);      
      Context context = getOutScope(scope, this).getContext();
      Object existingDataModel = context.get(name);
      
      boolean dirty = existingDataModel == null ||
            wrapper.isDirty(dataModelAnn, existingDataModel, list);
      boolean reoutject = existingDataModel!=null && scope==PAGE;
      
      if (dirty)
      {
         if ( list!=null )
         {
            context.set( name, wrapper.wrap(dataModelAnn, list) );
         }
         else
         {
            context.remove(name);
         }
      }
      else if (reoutject)
      {
         context.set(name, existingDataModel);
      }
         
   }

   private static DataBinder createWrapper(Annotation dataModelAnn)
   {
      try
      {
         return dataModelAnn.annotationType().getAnnotation(DataBinderClass.class).value().newInstance();
      }
      catch (Exception e)
      {
         throw new RuntimeException(e);
      }
   }

   private static DataSelector createUnwrapper(Annotation dataModelAnn)
   {
      try
      {
         return dataModelAnn.annotationType().getAnnotation(DataSelectorClass.class).value().newInstance();
      }
      catch (Exception e)
      {
         throw new RuntimeException(e);
      }
   }

   private static ScopeType getOutScope(ScopeType specifiedScope, Component component)
   {
      ScopeType scope = component==null ? EVENT : component.getScope();
      if (scope==STATELESS)
      {
         scope = EVENT;
      }
      if (specifiedScope!=UNSPECIFIED)
      {
         scope = specifiedScope;
      }
      return scope;
   }

   private void injectAttributes(Object bean, boolean enforceRequired)
   {
      for ( BijectedAttribute<In> att : getInAttributes() )
      {
         att.set( bean, getValueToInject( att.getAnnotation(), att.getName(), bean, enforceRequired ) );
      }
   }

   private void disinjectAttributes(Object bean)
   {
      for ( BijectedAttribute att: getInAttributes() )
      {
         if ( !att.getType().isPrimitive() )
         {
            att.set(bean, null);
         }
      }
   }

   private void outjectAttributes(Object bean, boolean enforceRequired)
   {
      for ( BijectedAttribute<Out> att: getOutAttributes() )
      {
         outjectAttribute( att.getAnnotation(), att.getName(), bean, att.get(bean), enforceRequired );
      }
   }

   private void outjectAttribute(Out out, String name, Object bean, Object value, boolean enforceRequired)
   {
      
      if (value==null && enforceRequired && out.required())
      {
         throw new RequiredException(
               "@Out attribute requires non-null value: " +
               getAttributeMessage(name)
            );
      }
      else
      {
         Component component = null;
         if ( out.scope()==UNSPECIFIED )
         {
            component = Component.forName(name);
            if (value!=null && component!=null)
            {
               if ( !component.isInstance(value) )
               {
                  throw new IllegalArgumentException(
                        "attempted to bind an @Out attribute of the wrong type to: " +
                        getAttributeMessage(name)
                     );
               }
            }
         }
         else if ( out.scope()==STATELESS )
         {
            throw new IllegalArgumentException(
                  "cannot specify explicit scope=STATELESS on @Out: " +
                  getAttributeMessage(name)
               );
         }
      
         ScopeType outScope = component==null ?
               getOutScope( out.scope(), this ) :
               component.getScope();
      
         if ( enforceRequired || outScope.isContextActive() )
         {
            if (value==null)
            {
               outScope.getContext().remove(name);
            }
            else
            {
               outScope.getContext().set(name, value);
            }
         }
      }
   }

   public boolean isInstance(Object bean)
   {
      switch(type)
      {
         case JAVA_BEAN:
         case ENTITY_BEAN:
            return getBeanClass().isInstance(bean);
         default:
            Class clazz = bean.getClass();
            for ( Class businessInterface: businessInterfaces )
            {
               if ( businessInterface.isAssignableFrom(clazz) )
               {
                  return true;
               }
            }
            
            // No interface view
            if (clazz.isInstance(bean)) {
               return true;
            }
            
            return false;
      }
   }

   public static Set<Class> getBusinessInterfaces(Class clazz)
   {
      Set<Class> result = new LinkedHashSet<Class>();

      if ( clazz.isAnnotationPresent(LOCAL) )
      {
         for ( Class iface: value( clazz.getAnnotation(LOCAL) ) ) {
            result.add(iface);
         }
      }

      if ( clazz.isAnnotationPresent(REMOTE) )
      {
         for ( Class iface: value( clazz.getAnnotation(REMOTE) ) )
         {
            result.add(iface);
         }
      }

      for ( Class iface: clazz.getInterfaces() )
      {
         if ( iface.isAnnotationPresent(LOCAL) || iface.isAnnotationPresent(REMOTE) )
         {
            result.add(iface);
         }
      }

      if ( result.isEmpty() ) 
      {
         for ( Class iface: clazz.getInterfaces() )
         {
            if ( !isExcludedLocalInterfaceName( iface.getName() ) )
            {
               result.add(iface);
            }
         }
      }

      return result;
   }

   public Set<Class> getBusinessInterfaces()
   {
      return businessInterfaces;
   }

   private static boolean isExcludedLocalInterfaceName(String name) 
   {
      return name.equals("java.io.Serializable") ||
            name.equals("java.io.Externalizable") ||
            name.startsWith("javax.ejb.");
   }

   private Object getFieldValue(Object bean, Field field, String name)
   {
      try {
         return Reflections.get(field, bean);
      }
      catch (Exception e)
      {
         throw new IllegalArgumentException("could not get field value: " + getAttributeMessage(name), e);
      }
   }

   private Object getPropertyValue(Object bean, Method method, String name)
   {
      try {
         return Reflections.invoke(method, bean);
      }
      catch (Exception e)
      {
         throw new IllegalArgumentException("could not get property value: " + getAttributeMessage(name), e);
      }
   }

   private void setPropertyValue(Object bean, Method method, String name, Object value)
   {
      try
      {
         Reflections.invoke(method, bean, value );
      }
      catch (Exception e)
      {
         throw new IllegalArgumentException("could not set property value: " + getAttributeMessage(name), e);
      }
   }

   private void setFieldValue(Object bean, Field field, String name, Object value)
   {
      try
      {
         Reflections.set(field, bean, value);
      }
      catch (Exception e)
      {
         throw new IllegalArgumentException("could not set field value: " + getAttributeMessage(name), e);
      }
   }

   public static String getComponentName(Class<?> clazz)
   {
      String componentName = Seam.getComponentName(clazz);
      if (componentName==null)
      {
         throw new IllegalArgumentException("No @Name annotation for class: " + clazz.getName());
      }
      return componentName;
   }

   public static Component forName(String name)
   {
      if ( !Contexts.isApplicationContextActive() )
      {
         throw new IllegalStateException("No application context active");
      }
      return (Component) Contexts.getApplicationContext().get( name + ".component" );
   }

   public static Object getInstance(Class<?> clazz)
   {
      return getInstance(clazz, true);
   }

   public static Object getInstance(Class<?> clazz, boolean create)
   {
      return getInstance( getComponentName(clazz), create );
   }

   public static Object getInstance(Class<?> clazz, ScopeType scope)
   {
      return getInstance(clazz, scope, true);
   }

   public static Object getInstance(Class<?> clazz, ScopeType scope, boolean create)
   {
      return getInstance( getComponentName(clazz), scope, create );
   }

   public static Object getInstance(String name)
   {
      return getInstance(name, true);
   }

   public static Object getInstance(String name, boolean create)
   {
      return getInstance(name, create, true);
   }
   
   public static Object getInstance(String name, boolean create, boolean allowAutocreation)
   {
      Object result = Contexts.lookupInStatefulContexts(name);
      result = getInstance(name, create, allowAutocreation, result, null);
      return result;
   }

   public static Object getInstance(String name, ScopeType scope)
   {
      return getInstance(name, scope, true);
   }

   public static Object getInstance(String name, ScopeType scope, boolean create)
   {
      return getInstance(name, scope, create, true);
   }
   
   public static Object getInstance(String name, ScopeType scope, boolean create, boolean allowAutocreation)
   {
      Object result = scope==STATELESS ? null : scope.getContext().get(name);
      result = getInstance(name, create, allowAutocreation, result, scope);
      return result;
   }

   private static Object getInstance(String name, boolean create, boolean allowAutoCreation, Object result, ScopeType scope) {
      Component component = Component.forName(name);

      create = create || (Init.instance().isAutocreateVariable(name) && allowAutoCreation);

      if (result==null && create)
      {
        result = getInstanceFromFactory(name, scope);
        if (result==null)
        {
           if (component==null)
           {
              //needed when this method is called by JSF
              if ( log.isTraceEnabled() ) log.trace("Seam component not found: " + name);
           }
           else if ( component.getScope().isContextActive() )
           {
              result = component.newInstance();
           }
           else
           {
              log.warn("Cannot create Seam component, scope is not active: " + name + "(" + component.getScope().name() + ")");
           }
        }
      }

      if (result!=null)
      {
         if (component!=null)
         {
            if ( !component.isInstance(result) )
            {
               if ( component.hasUnwrapMethod() ) return result; ///best way???
               throw new IllegalArgumentException( "value of context variable is not an instance of the component bound to the context variable: " + name  +
                        ". If you are using hot deploy, you may have attempted to hot deploy a session or " +
                        "application-scoped component definition while using an old instance in the session.");
            }
            result = component.unwrap(result);
         }
      }

      return result;

   }

   public static Object getInstanceFromFactory(String name)
   {
      return getInstanceFromFactory(name, null);
   }

   private static Object getInstanceFromFactory(String name, ScopeType scope)
   {
      Init init = Init.instance();
      if (init == null) // for unit tests, yew!
      {
         return null;
      }
      else
      {
         Init.FactoryMethod factoryMethod = init.getFactory(name);
         Init.FactoryExpression methodBinding = init.getFactoryMethodExpression(name);
         Init.FactoryExpression valueBinding = init.getFactoryValueExpression(name);
         if (methodBinding != null && getOutScope(methodBinding.getScope(), null).isContextActive())
         {// let the XML take precedence
            Object result = methodBinding.getMethodBinding().invoke();
            return handleFactoryMethodResult(name, null, result, methodBinding.getScope());
         }
         else if (valueBinding != null && getOutScope(valueBinding.getScope(), null).isContextActive()) 
         { // let the XML take precedence
            Object result = valueBinding.getValueBinding().getValue();
            return handleFactoryMethodResult(name, null, result, valueBinding.getScope());
         }
         else if (factoryMethod != null && getOutScope(factoryMethod.getScope(), factoryMethod.getComponent()).isContextActive())
         {
            Object factory = Component.getInstance(factoryMethod.getComponent().getName(), true);
            ScopeType scopeResult = getOutScope(factoryMethod.getScope(), factoryMethod.getComponent());
            ScopeType scopeFactory = factoryMethod.getComponent().getScope();
            // we need this lock in the following cases: (1) the target scope is
            // accessed by more than one thread (as we don't want to create the
            // same object by two threads at the same time for the same scope)
            // OR (2) the factory is accessed by more than one thread and is
            // using interceptors (as accessing a factory multiple times might
            // mess up interceptors). This assumes that (1) the scopes
            // CONVERSATION, EVENT and PAGE can't be accessed by more than one
            // thread anyway due to CONVERSATION being locked for the current
            // thread anyway, and EVENT and PAGE being only short-lived anyway.
            // (2) a factory that doesn't use injection can be accessed multi
            // threaded. See JBSEAM-4669/JBSEAM-2419 for the original
            // discussion.
            boolean lockingNeeded = ((scopeResult != ScopeType.CONVERSATION && scopeResult != ScopeType.EVENT && scopeResult != ScopeType.PAGE) ||
                  (scopeFactory != ScopeType.CONVERSATION && scopeFactory != ScopeType.EVENT && scopeFactory != ScopeType.PAGE && factoryMethod.getComponent().interceptionEnabled));

            if (lockingNeeded)
            {
              // Only one factory instance can access result scope
              // CONVERSATION / EVENT / PAGE anyway due to
              // the locking of the conversation.
              synchronized (factoryMethod)
              {
                 return createInstanceFromFactory(name, scope, factoryMethod, factory);
              }
            }
            else
            {
               return createInstanceFromFactory(name, scope, factoryMethod, factory);
            }
         }
         else
         {
            return null;
         }
      }
   }

   private static Object createInstanceFromFactory(String name, ScopeType scope, Init.FactoryMethod factoryMethod, Object factory)
   {
      // check whether there has been created an instance by another thread
      // while waiting for this function's lock
      if (scope != STATELESS)
      {
         Object value = (scope == null) ? Contexts.lookupInStatefulContexts(name) : scope.getContext().get(name);
         if (value != null)
         {
            return value;
         }
      }

      if (factory == null)
      {
         return null;
      }
      else
      {
         Object result = factoryMethod.getComponent().callComponentMethod(factory, factoryMethod.getMethod());
         return handleFactoryMethodResult(name, factoryMethod.getComponent(), result, factoryMethod.getScope());
      }
   }
   
   private static Object handleFactoryMethodResult(String name, Component component, Object result, ScopeType scope) {
        // see if a value was outjected by the factory method
        Object value = Contexts.lookupInStatefulContexts(name);
        if (value == null) {
            // usually a factory method returning a value
            ScopeType outScope = getOutScope(scope, component);
            if (outScope != STATELESS) {
                // the null check is for page scope, which doesn't handle setting a null value
                if (result != null) {
                    outScope.getContext().set(name, result);
                }
                // shouldn't need to call remove since there wasn't previously a value
            }
            return result;
        } else { 
            // usually a factory method with a void return type
            if (scope != UNSPECIFIED) {
                throw new IllegalArgumentException("factory method with defined scope outjected a value: " + name);
            }
            return value;
        }
    }

   public Object newInstance()
   {
      if (log.isTraceEnabled()) {
         log.trace("instantiating Seam component: " + name);
      }

      Object instance;
      try{
         instance = instantiate();
          
         if (getScope()!=STATELESS) {
            //put it in the context _before_ calling postconstuct or create
            getScope().getContext().set(name, instance); 
         }
         
         postConstruct(instance);
            
         if (getScope()!=STATELESS) {
            callCreateMethod(instance);
            
            if (Events.exists()) {
                Events.instance().raiseEvent("org.jboss.seam.postCreate." + name, instance);
            }
         }
         
      } catch (Exception e) {
              if (getScope()!=STATELESS) {
                      getScope().getContext().remove(name); 
              }

              throw new InstantiationException("Could not instantiate Seam component: " + name, e);
      }

      return instance;
   }

   private void callDefaultRemoveMethod(Object instance)
   {
      if ( hasDefaultRemoveMethod() )
      {
         callComponentMethod( instance, getDefaultRemoveMethod() );
      }
   }
   
   public boolean hasDefaultRemoveMethod()
   {
      return defaultRemoveMethod!=null;
   }

   public Method getDefaultRemoveMethod()
   {
      return defaultRemoveMethod;
   }

   public void callCreateMethod(Object instance)
   {
      if ( hasCreateMethod() )
      {
         callComponentMethod( instance, getCreateMethod() );
      }
   }

   public void callDestroyMethod(Object instance)
   {
      if ( hasDestroyMethod() )
      {
         callComponentMethod( instance, getDestroyMethod() );
      }
   }

   public void callPreDestroyMethod(Object instance)
   {
      if ( hasPreDestroyMethod() )
      {
         callComponentMethod( instance, getPreDestroyMethod() );
      }
   }

   public void callPostConstructMethod(Object instance)
   {
      if ( hasPostConstructMethod() )
      {
         callComponentMethod( instance, getPostConstructMethod() );
      }
   }

   public void callPrePassivateMethod(Object instance)
   {
      if ( hasPrePassivateMethod() )
      {
         callComponentMethod( instance, getPrePassivateMethod() );
      }
   }

   public void callPostActivateMethod(Object instance)
   {
      if ( hasPostActivateMethod() )
      {
         callComponentMethod( instance, getPostActivateMethod() );
      }
   }

   public Method getPostActivateMethod()
   {
      return postActivateMethod;
   }

   public Method getPrePassivateMethod()
   {
      return prePassivateMethod;
   }

   public Method getPostConstructMethod()
   {
      return postConstructMethod;
   }

   public Method getPreDestroyMethod()
   {
      return preDestroyMethod;
   }

   public long getTimeout()
   {
      return timeout;
   }

   public Object callComponentMethod(Object instance, Method method, Object... parameters) {
      Class[] paramTypes = method.getParameterTypes();
      String methodName = method.getName();
      try
      {
         Method interfaceMethod = instance.getClass().getMethod(methodName, paramTypes);
         if ( paramTypes.length==0 || interfaceMethod.getParameterTypes().length==0 )
         {
            return Reflections.invokeAndWrap(interfaceMethod, instance);
         }
         else if ( parameters.length>0 )
         {
            return Reflections.invokeAndWrap(interfaceMethod, instance, parameters);
         }
         else
         {
            return Reflections.invokeAndWrap(interfaceMethod, instance, this);
         }
      }
      catch (NoSuchMethodException e)
      {
         String message = "method not found: " + method.getName() + " for component: " + name;
         if ( getType().isSessionBean() )
         {
             message += " (check that it is declared on the session bean business interface)";
         }
         throw new IllegalArgumentException(message, e);
      }
   }

   private Object unwrap(Object instance)
   {
      if ( hasUnwrapMethod() )
      {
         return callComponentMethod( instance, getUnwrapMethod() );
      }
      else
      {
         return instance;
      }
   }

   private Object getValueToInject(In in, String name, Object bean, boolean enforceRequired)
   {
      Object result;
      if ( name.startsWith("#") )
      {
         if ( log.isDebugEnabled() )
         {
            log.debug("trying to inject with EL expression: " + name);
         }
         result = Expressions.instance().createValueExpression(name).getValue();
      }
      else if ( in.scope()==UNSPECIFIED )
      {
         if ( log.isDebugEnabled() )
         {
            log.debug("trying to inject with hierarchical context search: " + name);
         }
         boolean allowAutocreation = !org.jboss.seam.contexts.Lifecycle.isDestroying();
         boolean create = in.create() && allowAutocreation;
         result = getInstanceInAllNamespaces(name, create, allowAutocreation);
      }
      else
      {
         if ( in.create() )
         {
            throw new IllegalArgumentException(
                  "cannot combine create=true with explicit scope on @In: " +
                  getAttributeMessage(name)
               );
         }
         if ( in.scope()==STATELESS )
         {
            throw new IllegalArgumentException(
                  "cannot specify explicit scope=STATELESS on @In: " +
                  getAttributeMessage(name)
               );
         }
         if ( log.isDebugEnabled() )
         {
            log.debug("trying to inject from specified context: " + name + ", scope: " + scope);
         }
         if ( enforceRequired || in.scope().isContextActive() )
         {
            result = in.scope().getContext().get(name);
         }
         else
         {
            return null;
         }
      }

      if ( result==null && enforceRequired && in.required() )
      {
         throw new RequiredException(
               "@In attribute requires non-null value: " +
               getAttributeMessage(name)
            );
      }
      else
      {
         return result;
      }
   }

   private Object getInstanceInAllNamespaces(String name, boolean create, boolean allowAutocreation)
   {
      Object result;
      result = getInstance(name, create, allowAutocreation);
      if (result==null)
      {
         for ( Namespace namespace: getImports() )
         {
            result = namespace.getComponentInstance(name, create, allowAutocreation);
            if (result!=null) break; 
         }
      }
      if (result==null)
      {
         for ( Namespace namespace: Init.instance().getGlobalImports() )
         {
            result = namespace.getComponentInstance(name, create, allowAutocreation);
            if (result!=null) break; 
         }
      }
      if (result==null)
      {
         Namespace namespace = getNamespace();
         if (namespace!=null)
         {
            result = namespace.getComponentInstance(name, create, allowAutocreation);
         }
      }
      return result;
   }

   private String getAttributeMessage(String attributeName)
   {
      return getName() + '.' + attributeName;
   }

   private static String toName(String name, Method method)
   {
      //TODO: does not handle "isFoo"
      if (name==null || name.length() == 0)
      {
         name = method.getName().substring(3, 4).toLowerCase()
               + method.getName().substring(4);
      }
      return name;
   }

   private static String toName(String name, Field field)
   {
      if (name==null || name.length() == 0)
      {
         name = field.getName();
      }
      return name;
   }

   @Override
   public String toString()
   {
      return "Component(" + name + ")";
   }

   public static Class<ProxyObject> createProxyFactory(ComponentType type, final Class beanClass, Collection<Class> businessInterfaces)
   {
      Set<Class> interfaces = new LinkedHashSet<Class>();
      interfaces.add(Instance.class);
      interfaces.add(Proxy.class);
      
      boolean noInterfaceView = false;
      
      if ( type.isSessionBean() )
      {
          if (businessInterfaces.isEmpty())
          {
             noInterfaceView = true;
          }
          else
          {
             interfaces.addAll(businessInterfaces);
          }
      }
      else
      {
         interfaces.add(HttpSessionActivationListener.class);
         interfaces.add(Mutable.class);
      }
      ProxyFactory factory = new ProxyFactory();
      factory.setSuperclass( (type==JAVA_BEAN || noInterfaceView) ? beanClass : Object.class );
      factory.setInterfaces( interfaces.toArray( new Class[0] ) );
      factory.setFilter(FINALIZE_FILTER);
      return factory.createClass();
   }

   private static final MethodFilter FINALIZE_FILTER = new MethodFilter() 
   {
      public boolean isHandled(Method method) 
      {
         // skip finalize methods
         return method.getParameterTypes().length!=0 || !method.getName().equals( "finalize" );
      }
   };
      
   public boolean isInterceptionEnabled()
   {
      return interceptionEnabled;
   }

   public boolean isStartup() 
   {
      return startup;
   }
   
   public boolean isSynchronize()
   {
      return synchronize;
   }

   public String[] getDependencies()
   {
      return dependencies;
   }

   public boolean isLifecycleMethod(Method method)
   {
      return method==null || //EJB 3 JavaDoc says InvocationContext.getMethod() returns null for lifecycle callbacks!
            lifecycleMethods.contains(method);
   }
   
   public boolean isConversationManagementMethod(Method method)
   {
      return method!=null && 
            conversationManagementMethods.contains(method);
   }

   static interface InitialValue
   {
      Object getValue(Class type);
   }

   static class ConstantInitialValue implements InitialValue
   {
      private Object value;

      public ConstantInitialValue(PropertyValue propertyValue, Class parameterClass, Type parameterType)
      {
         this.value = Conversions.getConverter(parameterClass).toObject(propertyValue, parameterType);
      }

      public Object getValue(Class type)
      {
         return value;
      }

      @Override
      public String toString()
      {
         return "ConstantInitialValue(" + value + ")";
      }

   }

   static class ELInitialValue implements InitialValue
   {
      private String expression;
      //private ValueBinding vb;
      private Conversions.Converter converter;
      private Type parameterType;

      public ELInitialValue(PropertyValue propertyValue, Class parameterClass, Type parameterType)
      {
         this.expression = propertyValue.getSingleValue();
         this.parameterType = parameterType;
         try
         {
            this.converter = Conversions.getConverter(parameterClass);
         }
         catch (IllegalArgumentException iae) {
            //no converter for the type
         }
         //vb = FacesContext.getCurrentInstance().getApplication().createValueBinding(expression);
      }

      public Object getValue(Class type)
      {
         Object value;
         if ( type.equals(ValueExpression.class) )
         {
            value = createValueExpression();
         }
         else if ( type.equals(MethodExpression.class) )
         {
            value = createMethodExpression();
         }
         else
         {
            value = createValueExpression().getValue();
         }

         if (converter!=null && value instanceof String)
         {
            return converter.toObject( new Conversions.FlatPropertyValue( (String) value ), parameterType );
         }
         else if (converter!=null && value instanceof String[])
         {
            return converter.toObject( new Conversions.MultiPropertyValue( (String[]) value, null), parameterType );
         }
         else
         {
            return value;
         }
      }

      private ValueExpression createValueExpression()
      {
         return Expressions.instance().createValueExpression(expression);
      }

      private MethodExpression createMethodExpression()
      {
         return Expressions.instance().createMethodExpression(expression);
      }

      @Override
      public String toString()
      {
         return "ELInitialValue(" + expression + ")";
      }

   }

   static class SetInitialValue implements InitialValue
   {
      private InitialValue[] initialValues;
      private Class elementType;
      private Class collectionClass;
      
      public SetInitialValue(PropertyValue propertyValue, Class collectionClass, Type collectionType)
      {
         String[] expressions = propertyValue.getMultiValues();
         initialValues = new InitialValue[expressions.length];
         elementType = Reflections.getCollectionElementType(collectionType);
         if (propertyValue.getType() != null)
         {
            this.collectionClass = propertyValue.getType();
         }
         else
         {
            this.collectionClass = collectionClass;
         }
         for ( int i=0; i<expressions.length; i++ )
         {
            PropertyValue elementValue = new Conversions.FlatPropertyValue( expressions[i] );
            initialValues[i] = getInitialValue(elementValue, elementType, elementType);
         }
      }

      public Object getValue(Class type)
      {
          Set set;
          //if no configuration has been specified then we first see if
          //the property is an abstract type, if so we create it using newInstance
          if (Modifier.isAbstract(collectionClass.getModifiers()) || Modifier.isInterface(collectionClass.getModifiers()))
          {
             if(collectionClass == SortedSet.class)
             {
                set = new TreeSet();
             }
             else
             {
                set = new LinkedHashSet(initialValues.length);
             }
          }
          else
          {
             try
             {
                set = (Set) collectionClass.newInstance();
             }
             catch (IllegalAccessException e)
             {
                throw new IllegalArgumentException("Cannot instantiate a set of type " + collectionClass + "; try specifying type type in components.xml");
             }
             catch (ClassCastException e)
             {
                throw new IllegalArgumentException("Cannot cast " + collectionClass + " to java.util.Set");
             }
             catch (java.lang.InstantiationException e)
             {
                throw new IllegalArgumentException("Cannot instantiate a set of type " + collectionClass + "; try specifying type type in components.xml");
             }
          }
          for (InitialValue iv: initialValues) 
          {
             set.add( iv.getValue(elementType) );
          }
          return set;
      }
      
      @Override
      public String toString()
      {
         return "SetInitialValue(" + elementType.getSimpleName() + ")";
      }
   }
   
   static class ListInitialValue implements InitialValue
   {
      private InitialValue[] initialValues;
      private Class elementType;
      private boolean isArray;
      private Class collectionClass;

      public ListInitialValue(PropertyValue propertyValue, Class collectionClass, Type collectionType)
      {
         String[] expressions = propertyValue.getMultiValues();
         initialValues = new InitialValue[expressions.length];
         isArray = collectionClass.isArray();
         elementType = isArray ? 
                  collectionClass.getComponentType() : 
                  Reflections.getCollectionElementType(collectionType);
         if (propertyValue.getType() != null)
         {
            this.collectionClass = propertyValue.getType();
         }
         else
         {
            this.collectionClass = collectionClass;
         }
         for ( int i=0; i<expressions.length; i++ )
         {
            PropertyValue elementValue = new Conversions.FlatPropertyValue( expressions[i] );
            initialValues[i] = getInitialValue(elementValue, elementType, elementType);
         }
      }

      public Object getValue(Class type)
      {
         if (isArray)
         {
            Object array = Array.newInstance(elementType, initialValues.length);
            for (int i=0; i<initialValues.length; i++)
            {
               Array.set( array, i, initialValues[i].getValue(elementType) );
            }
            return array;
         }
         else
         {
            List list;
            //if no configuration has been specified then we first see if
            //the property is an abstract type, if so we create it using newInstance
            if (Modifier.isAbstract(collectionClass.getModifiers()) || Modifier.isInterface(collectionClass.getModifiers()))
            {
               list = new ArrayList(initialValues.length);
            }
            else
            {
               try
               {
                  list = (List) collectionClass.newInstance();
               }
               catch (IllegalAccessException e)
               {
                  throw new IllegalArgumentException("Cannot instantiate a list of type " + collectionClass + "; try specifying type type in components.xml");
               }
               catch (ClassCastException e)
               {
                  throw new IllegalArgumentException("Cannot cast " + collectionClass + " to java.util.List");
               }
               catch (java.lang.InstantiationException e)
               {
                  throw new IllegalArgumentException("Cannot instantiate a list of type " + collectionClass + "; try specifying type type in components.xml");
               }
            }
            for (InitialValue iv: initialValues)
            {
               list.add( iv.getValue(elementType) );
            }
            return list;
         }
      }
      
      @Override
      public String toString()
      {
         return "ListInitialValue(" + elementType.getSimpleName() + ")";
      }

   }
   
   static class MapInitialValue implements InitialValue
   {
      private Map<InitialValue, InitialValue> initialValues;
      private Class elementType;
      private Class keyType;
      private Class collectionClass;
      
      public MapInitialValue(PropertyValue propertyValue, Class collectionClass, Type collectionType)
      {
         Map<String, String> expressions = propertyValue.getKeyedValues();
         initialValues = new LinkedHashMap<InitialValue, InitialValue>(expressions.size());      
         elementType = Reflections.getCollectionElementType(collectionType);
         keyType = Reflections.getMapKeyType(collectionType);
         if (propertyValue.getType() != null )
         {
            this.collectionClass = propertyValue.getType();
         }
         else
         {
            this.collectionClass = collectionClass;
         }
         for ( Map.Entry<String, String> me: expressions.entrySet() )
         {
            PropertyValue keyValue = new Conversions.FlatPropertyValue( me.getKey() );
            PropertyValue elementValue = new Conversions.FlatPropertyValue( me.getValue() );
            initialValues.put( getInitialValue(keyValue, keyType, keyType), getInitialValue(elementValue, elementType, elementType) ); 
         }
      }

      public Object getValue(Class type)
      {
         Map result;
         if (Modifier.isAbstract(collectionClass.getModifiers()) || Modifier.isInterface(collectionClass.getModifiers()))
         {
            if (collectionClass == SortedMap.class)
            {
               result = new TreeMap();
            }
            else
            {
               result = new LinkedHashMap(initialValues.size());
            }
         }
         else
         {
            try
            {
               result = (Map) collectionClass.newInstance();
            }
            catch (IllegalAccessException e)
            {
               throw new IllegalArgumentException("Cannot instantiate a map of type " + collectionClass + "; try specifying type type in components.xml");
            }
            catch (ClassCastException e)
            {
               throw new IllegalArgumentException("Cannot cast " + collectionClass + " to java.util.Map");
            }
            catch (java.lang.InstantiationException e)
            {
               throw new IllegalArgumentException("Cannot instantiate a map of type " + collectionClass + "; try specifying type type in components.xml");
            }
         }
        
         for ( Map.Entry<InitialValue, InitialValue> me : initialValues.entrySet() )
         {
            result.put( me.getKey().getValue(keyType), me.getValue().getValue(elementType) );
         }
         return result;
      }
      
      @Override
      public String toString()
      {
         return "MapInitialValue(" + keyType.getSimpleName() + "," + elementType.getSimpleName() + ")";
      }

   }
   
   public interface BijectedAttribute<T extends Annotation>
   {
      public String getName();
      public T getAnnotation();
      public Class getType();
      public void set(Object bean, Object value);
      public Object get(Object bean);
   }

   final class BijectedMethod<T extends Annotation> implements BijectedAttribute<T>
   {
      private final String name;
      private final Method method;
      private final T annotation;
      
      private BijectedMethod(String name, Method method, T annotation)
      {
         this.name = name;
         this.method = method;
         this.annotation = annotation;
      }
      public String getName()
      {
         return name;
      }
      public Method getMethod()
      {
         return method;
      }
      public T getAnnotation()
      {
         return annotation;
      }
      public void set(Object bean, Object value)
      {
         setPropertyValue(bean, method, name, value);
      }
      public Object get(Object bean)
      {
         return getPropertyValue(bean, method, name);
      }
      public Class getType()
      {
         return method.getParameterTypes()[0];
      }
      @Override
      public String toString()
      {
         return "BijectedMethod(" + name + ')';
      }
   }
   
   final class BijectedProperty<T extends Annotation> implements BijectedAttribute<T>
   {
      
      private BijectedMethod<T> getter;
      private BijectedMethod<T> setter;
      
      public BijectedProperty(String name, Method getter, Method setter, T annotation)
      {
         this.getter = new BijectedMethod(name, getter, annotation);
         this.setter = new BijectedMethod(name, setter, annotation);
      }
      
      public BijectedProperty(String name, Method getter, T annotation)
      {
         this.getter = new BijectedMethod(name, getter, annotation);
         try
         {
            Method setterMethod = Reflections.getSetterMethod(getter.getDeclaringClass(), name);
            this.setter = new BijectedMethod(name, setterMethod, annotation);
         }
         catch (IllegalArgumentException e) {}        
      }

      public Object get(Object bean)
      {
         return getter.get(bean);
      }

      public T getAnnotation()
      {
         return getter.getAnnotation();
      }

      public String getName()
      {
         return getter.getName();
      }

      public Class getType()
      {
         return getter.getType();
      }

      public void set(Object bean, Object value)
      {
         if (setter == null)
         {
            throw new IllegalArgumentException("Component must have a setter for " + name);
         }
         setter.set(bean, value); 
      }
      
   }
   
   final class BijectedField<T extends Annotation> implements BijectedAttribute<T>
   {
      private final String name;
      private final Field field;
      private final T annotation;
      
      private BijectedField(String name, Field field, T annotation)
      {
         this.name = name;
         this.field = field;
         this.annotation = annotation;
      }
      public String getName()
      {
         return name;
      }
      public Field getField()
      {
         return field;
      }
      public T getAnnotation()
      {
         return annotation;
      }
      public Class getType()
      {
         return field.getType();
      }
      public void set(Object bean, Object value)
      {
         setFieldValue(bean, field, name, value);
      }
      public Object get(Object bean)
      {
         return getFieldValue(bean, field, name);
      }
      @Override
      public String toString()
      {
         return "BijectedField(" + name + ')';
      }
   }

   public List<BijectedAttribute> getPersistenceContextAttributes()
   {
      return pcAttributes;
   }

   public Collection<Namespace> getImports()
   {
      return imports;
   }

   public Namespace getNamespace()
   {
      return namespace;
   }
   
   public boolean isPerNestedConversation()
   {
      return perNestedConversation;
   }
   
   public boolean hasConversationManagementMethods()
   {
      return !conversationManagementMethods.isEmpty();
   }
   
   public boolean isSecure()
   {
      return secure;
   }
}
