/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.apache.openejb.config;

import org.apache.openejb.ClassLoaderUtil;
import org.apache.openejb.OpenEJBException;
import org.apache.openejb.OpenEJBRuntimeException;
import org.apache.openejb.assembler.classic.AppInfo;
import org.apache.openejb.assembler.classic.ClassListInfo;
import org.apache.openejb.assembler.classic.ClientInfo;
import org.apache.openejb.assembler.classic.ConnectorInfo;
import org.apache.openejb.assembler.classic.EjbJarInfo;
import org.apache.openejb.assembler.classic.EnterpriseBeanInfo;
import org.apache.openejb.assembler.classic.EntityManagerFactoryCallable;
import org.apache.openejb.assembler.classic.FilterInfo;
import org.apache.openejb.assembler.classic.HandlerChainInfo;
import org.apache.openejb.assembler.classic.IdPropertiesInfo;
import org.apache.openejb.assembler.classic.JndiEncInfo;
import org.apache.openejb.assembler.classic.ListenerInfo;
import org.apache.openejb.assembler.classic.MdbContainerInfo;
import org.apache.openejb.assembler.classic.MessageDrivenBeanInfo;
import org.apache.openejb.assembler.classic.ParamValueInfo;
import org.apache.openejb.assembler.classic.PersistenceUnitInfo;
import org.apache.openejb.assembler.classic.PortInfo;
import org.apache.openejb.assembler.classic.ResourceInfo;
import org.apache.openejb.assembler.classic.ServiceInfo;
import org.apache.openejb.assembler.classic.ServletInfo;
import org.apache.openejb.assembler.classic.ValidatorBuilder;
import org.apache.openejb.assembler.classic.WebAppInfo;
import org.apache.openejb.config.event.BeforeAppInfoBuilderEvent;
import org.apache.openejb.config.sys.Container;
import org.apache.openejb.config.sys.Resource;
import org.apache.openejb.config.sys.Service;
import org.apache.openejb.config.sys.ServiceProvider;
import org.apache.openejb.jee.AdminObject;
import org.apache.openejb.jee.ApplicationClient;
import org.apache.openejb.jee.ConfigProperty;
import org.apache.openejb.jee.ConnectionDefinition;
import org.apache.openejb.jee.Connector;
import org.apache.openejb.jee.EnterpriseBean;
import org.apache.openejb.jee.Filter;
import org.apache.openejb.jee.InboundResourceadapter;
import org.apache.openejb.jee.Listener;
import org.apache.openejb.jee.MessageListener;
import org.apache.openejb.jee.OutboundResourceAdapter;
import org.apache.openejb.jee.ParamValue;
import org.apache.openejb.jee.PortComponent;
import org.apache.openejb.jee.ResourceAdapter;
import org.apache.openejb.jee.ServiceImplBean;
import org.apache.openejb.jee.Servlet;
import org.apache.openejb.jee.TransactionSupportType;
import org.apache.openejb.jee.WebApp;
import org.apache.openejb.jee.WebserviceDescription;
import org.apache.openejb.jee.Webservices;
import org.apache.openejb.jee.jpa.EntityMappings;
import org.apache.openejb.jee.jpa.JpaJaxbUtil;
import org.apache.openejb.jee.jpa.unit.Persistence;
import org.apache.openejb.jee.jpa.unit.PersistenceUnit;
import org.apache.openejb.jee.oejb3.EjbDeployment;
import org.apache.openejb.jee.oejb3.OpenejbJar;
import org.apache.openejb.jee.oejb3.PojoDeployment;
import org.apache.openejb.jpa.integration.MakeTxLookup;
import org.apache.openejb.loader.SystemInstance;
import org.apache.openejb.persistence.PersistenceBootstrap;
import org.apache.openejb.util.CircularReferencesException;
import org.apache.openejb.util.LogCategory;
import org.apache.openejb.util.Logger;
import org.apache.openejb.util.Messages;
import org.apache.openejb.util.References;

import javax.xml.bind.JAXBException;
import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;

import static java.util.Arrays.asList;
import static java.util.Collections.singletonList;
import static org.apache.openejb.util.URLs.toFile;

/**
 * @version $Rev$ $Date$
 */
class AppInfoBuilder {

    private static final Logger logger = Logger.getInstance(LogCategory.OPENEJB_STARTUP_CONFIG, "org.apache.openejb.util.resources");
    private static final Messages messages = new Messages("org.apache.openejb.util.resources");

    private static boolean USE_EAR_AS_CONTEXT_ROOT_BASE = SystemInstance.get().getOptions().get("openejb.ear.use-as-webcontext-base", false);

    private final ConfigurationFactory configFactory;

    private EjbJarInfoBuilder ejbJarInfoBuilder = new EjbJarInfoBuilder();

    public AppInfoBuilder(final ConfigurationFactory configFactory) {
        this.configFactory = configFactory;
    }

    public AppInfo build(final AppModule appModule) throws OpenEJBException {
        // send an event so that it becomes pretty easy at this step to dynamically change the module description
        // before going into the info tree. Pretty easy to hack on portability issues.
        SystemInstance.get().fireEvent(new BeforeAppInfoBuilderEvent(appModule));

        final AppInfo appInfo = new AppInfo();
        appInfo.appId = appModule.getModuleId();
        appInfo.path = appModule.getJarLocation();
        appInfo.standaloneModule = appModule.isStandaloneModule();
        appInfo.delegateFirst = appModule.isDelegateFirst();
        appInfo.watchedResources.addAll(appModule.getWatchedResources());
        appInfo.mbeans.addAll(appModule.getAdditionalLibMbeans());
        appInfo.jaxRsProviders.addAll(appModule.getJaxRsProviders());
        appInfo.properties.putAll(appModule.getProperties());

        if (appInfo.appId == null) {
            throw new IllegalArgumentException("AppInfo.appId cannot be null");
        }
        if (appInfo.path == null) {
            appInfo.path = appInfo.appId;
        }

        this.buildPojoConfiguration(appModule, appInfo);

        this.buildAppResources(appModule, appInfo);
        this.buildAppServices(appModule, appInfo);

        //
        //  J2EE Connectors
        //
        this.buildConnectorModules(appModule, appInfo);

        //
        //  Persistence Units
        //
        this.buildPersistenceModules(appModule, appInfo);


        final List<String> containerIds = this.configFactory.getContainerIds();
        for (final ConnectorInfo connectorInfo : appInfo.connectors) {
            for (final MdbContainerInfo containerInfo : connectorInfo.inbound) {
                containerIds.add(containerInfo.id);
            }
        }

        //
        //  EJB Jars
        //
        final Map<EjbModule, EjbJarInfo> ejbJarInfos = new HashMap<EjbModule, EjbJarInfo>();
        for (final EjbModule ejbModule : appModule.getEjbModules()) {
            try {
                final EjbJarInfo ejbJarInfo = this.ejbJarInfoBuilder.buildInfo(ejbModule);
                ejbJarInfo.mbeans = ejbModule.getMbeans();

                final Map<String, EjbDeployment> deploymentsByEjbName = ejbModule.getOpenejbJar().getDeploymentsByEjbName();

                for (final EnterpriseBeanInfo bean : ejbJarInfo.enterpriseBeans) {
                    final EjbDeployment d = deploymentsByEjbName.get(bean.ejbName);

                    /*
                     * JRG - there's probably a better way of handling this, but this code handles the case when:
                     * 
                     * A connector with two or more inbound adapter is registered, causing two containers named with the format:
                     *     <moduleId>-<message listener interface>
                     * 
                     * This code adjusts the container id for the associated MDBs by sticking the message listener interface on the end.
                     * 
                     */
                    if (bean instanceof MessageDrivenBeanInfo && !containerIds.contains(d.getContainerId()) && !skipMdb(bean)) {
                        final MessageDrivenBeanInfo mdb = (MessageDrivenBeanInfo) bean;
                        final String newContainerId = d.getContainerId() + "-" + mdb.mdbInterface;
                        if (containerIds.contains(newContainerId)) {
                            d.setContainerId(newContainerId);
                        }
                    }

                    if (!containerIds.contains(d.getContainerId()) && !skipMdb(bean)) {
                        final String msg = messages.format("config.noContainerFound", d.getContainerId(), d.getEjbName());
                        logger.fatal(msg);
                        throw new OpenEJBException(msg);
                    }

                    bean.containerId = d.getContainerId();
                }


                for (final PojoDeployment pojoDeployment : ejbModule.getOpenejbJar().getPojoDeployment()) {
                    final IdPropertiesInfo info = new IdPropertiesInfo();
                    info.id = pojoDeployment.getClassName();
                    info.properties.putAll(pojoDeployment.getProperties());
                    ejbJarInfo.pojoConfigurations.add(info);
                }

                ejbJarInfo.validationInfo = ValidatorBuilder.getInfo(ejbModule.getValidationConfig());
                ejbJarInfo.portInfos.addAll(this.configureWebservices(ejbModule.getWebservices()));
                ejbJarInfo.uniqueId = ejbModule.getUniqueId();
                ejbJarInfo.webapp = ejbModule.isWebapp();
                this.configureWebserviceSecurity(ejbJarInfo, ejbModule);

                ejbJarInfos.put(ejbModule, ejbJarInfo);

                appInfo.ejbJars.add(ejbJarInfo);


            } catch (final OpenEJBException e) {
                ConfigUtils.logger.warning("conf.0004", ejbModule.getJarLocation(), e.getMessage());
                throw e;
            }
        }
        // Create the JNDI info builder
        final JndiEncInfoBuilder jndiEncInfoBuilder = new JndiEncInfoBuilder(appInfo);
        if (appModule.getApplication() != null) {
            //TODO figure out how to prevent adding stuff to the module and comp contexts from the application
            //or maybe validate the xml so this won't happen.
            jndiEncInfoBuilder.build(appModule.getApplication(), appInfo.appId, null, appModule.getModuleUri(), new JndiEncInfo(), new JndiEncInfo());
        }

        final List<EnterpriseBeanInfo> beans = new ArrayList<EnterpriseBeanInfo>();
        // Build the JNDI tree for each ejb
        for (final EjbModule ejbModule : appModule.getEjbModules()) {

            final EjbJarInfo ejbJar = ejbJarInfos.get(ejbModule);

            final Map<String, EnterpriseBean> beanData = ejbModule.getEjbJar().getEnterpriseBeansByEjbName();

            for (final EnterpriseBeanInfo beanInfo : ejbJar.enterpriseBeans) {
                beans.add(beanInfo);

                // Get the ejb-jar.xml object
                final EnterpriseBean enterpriseBean = beanData.get(beanInfo.ejbName);

                // Build the JNDI info tree for the EJB
                jndiEncInfoBuilder.build(enterpriseBean, beanInfo.ejbName, ejbJar.moduleName, ejbModule.getModuleUri(), ejbJar.moduleJndiEnc, beanInfo.jndiEnc);


                jndiEncInfoBuilder.buildDependsOnRefs(enterpriseBean, beanInfo, ejbJar.moduleName);
            }
        }

        // Check for circular references in Singleton @DependsOn
        try {
            References.sort(beans, new References.Visitor<EnterpriseBeanInfo>() {
                @Override
                public String getName(final EnterpriseBeanInfo bean) {
                    return bean.ejbDeploymentId;
                }

                @Override
                public Set<String> getReferences(final EnterpriseBeanInfo bean) {
                    return new LinkedHashSet<String>(bean.dependsOn);
                }
            });
        } catch (final CircularReferencesException e) {
            // List<List> circuits = e.getCircuits();
            // TODO Seems we lost circular reference detection, or we do it elsewhere and don't need it here
        }

        //
        //  Application Clients
        //
        this.buildClientModules(appModule, appInfo, jndiEncInfoBuilder);

        //
        //  Webapps
        //
        this.buildWebModules(appModule, jndiEncInfoBuilder, appInfo);


        //
        //  Final AppInfo creation
        //
        final List<URL> additionalLibraries = appModule.getAdditionalLibraries();
        for (final URL url : additionalLibraries) {
            final File file = toFile(url);
            try {
                appInfo.libs.add(file.getCanonicalPath());
            } catch (final IOException e) {
                throw new OpenEJBException("Invalid application lib path " + file.getAbsolutePath());
            }
        }

        if (appModule.getCmpMappings() != null) {
            try {
                appInfo.cmpMappingsXml = JpaJaxbUtil.marshal(EntityMappings.class, appModule.getCmpMappings());
            } catch (final JAXBException e) {
                throw new OpenEJBException("Unable to marshal cmp entity mappings", e);
            }
        }

        final ReportValidationResults reportValidationResults = new ReportValidationResults();
        reportValidationResults.deploy(appModule);

        logger.info("config.appLoaded", appInfo.path);

        appInfo.webAppAlone = appModule.isWebapp();

        return appInfo;

    }

    private void buildPojoConfiguration(final AppModule appModule, final AppInfo appInfo) {
        for (final Map.Entry<String, PojoConfiguration> config : appModule.getPojoConfigurations().entrySet()) {
            final IdPropertiesInfo info = new IdPropertiesInfo();
            info.id = config.getKey();
            info.properties.putAll(config.getValue().getProperties());
            appInfo.pojoConfigurations.add(info);
        }
    }

    private void buildAppServices(final AppModule appModule, final AppInfo appInfo) throws OpenEJBException {
        final Collection<Service> services = appModule.getServices();
        for (final Service service : services) {
            final ServiceInfo info = this.configFactory.configureService(service, ServiceInfo.class);
            appInfo.services.add(info);
        }
    }

    private void buildAppResources(final AppModule module, final AppInfo info) {
        for (final Resource def : module.getResources()) {
            // the resource is already deployed
            // however we keep its id to be able to undeployed it later
            // note: if ApplicationWide property was specified
            // we want this application be managed only by the container
            // once deployed = not undeployed with the app
            // so we skip the undeployement skipping the id
            if (!def.getProperties().containsKey("ApplicationWide")) {
                info.resourceIds.add(def.getId());
                info.resourceAliases.addAll(def.getAliases());
            }
        }
    }

    private void buildClientModules(final AppModule appModule, final AppInfo appInfo, final JndiEncInfoBuilder jndiEncInfoBuilder) throws OpenEJBException {
        for (final ClientModule clientModule : appModule.getClientModules()) {
            final ApplicationClient applicationClient = clientModule.getApplicationClient();
            final ClientInfo clientInfo = new ClientInfo();
            clientInfo.description = applicationClient.getDescription();
            clientInfo.displayName = applicationClient.getDisplayName();
            clientInfo.path = clientModule.getJarLocation();
            clientInfo.mainClass = clientModule.getMainClass();
            clientInfo.localClients.addAll(clientModule.getLocalClients());
            clientInfo.remoteClients.addAll(clientModule.getRemoteClients());
            clientInfo.callbackHandler = applicationClient.getCallbackHandler();
            clientInfo.moduleId = getClientModuleId(clientModule);
            clientInfo.watchedResources.addAll(clientModule.getWatchedResources());
            clientInfo.validationInfo = ValidatorBuilder.getInfo(clientModule.getValidationConfig());
            clientInfo.uniqueId = clientModule.getUniqueId();

            jndiEncInfoBuilder.build(applicationClient, clientModule.getJarLocation(), clientInfo.moduleId, clientModule.getModuleUri(), clientInfo.jndiEnc, clientInfo.jndiEnc);
            appInfo.clients.add(clientInfo);
        }
    }

    private void buildWebModules(final AppModule appModule, final JndiEncInfoBuilder jndiEncInfoBuilder, final AppInfo appInfo) throws OpenEJBException {
        for (final WebModule webModule : appModule.getWebModules()) {
            final WebApp webApp = webModule.getWebApp();
            final WebAppInfo webAppInfo = new WebAppInfo();
            webAppInfo.description = webApp.getDescription();
            webAppInfo.displayName = webApp.getDisplayName();
            webAppInfo.path = webModule.getJarLocation();
            webAppInfo.moduleId = webModule.getModuleId();
            webAppInfo.watchedResources.addAll(webModule.getWatchedResources());
            webAppInfo.validationInfo = ValidatorBuilder.getInfo(webModule.getValidationConfig());
            webAppInfo.uniqueId = webModule.getUniqueId();
            webAppInfo.restApplications.addAll(webModule.getRestApplications());
            webAppInfo.restClass.addAll(webModule.getRestClasses());
            webAppInfo.ejbWebServices.addAll(webModule.getEjbWebServices());
            webAppInfo.ejbRestServices.addAll(webModule.getEjbRestServices());
            webAppInfo.jaxRsProviders.addAll(webModule.getJaxrsProviders());

            for (final Map.Entry<String, Set<String>> entry : webModule.getWebAnnotatedClasses().entrySet()) {
                final ClassListInfo info = new ClassListInfo();
                info.name = entry.getKey();
                info.list.addAll(entry.getValue());
                webAppInfo.webAnnotatedClasses.add(info);
            }

            for (final Map.Entry<String, Set<String>> entry : webModule.getJsfAnnotatedClasses().entrySet()) {
                final ClassListInfo info = new ClassListInfo();
                info.name = entry.getKey();
                info.list.addAll(entry.getValue());
                webAppInfo.jsfAnnotatedClasses.add(info);
            }

            webAppInfo.host = webModule.getHost();

            if (!webModule.isStandaloneModule() && USE_EAR_AS_CONTEXT_ROOT_BASE) {
                webAppInfo.contextRoot = appModule.getModuleId() + "/" + webModule.getContextRoot();
            } else {
                webAppInfo.contextRoot = webModule.getContextRoot();
            }

            jndiEncInfoBuilder.build(webApp, webModule.getJarLocation(), webAppInfo.moduleId, webModule.getModuleUri(), webAppInfo.jndiEnc, webAppInfo.jndiEnc);

            webAppInfo.portInfos.addAll(this.configureWebservices(webModule.getWebservices()));
            // configureWebserviceSecurity(webAppInfo, webModule);: was empty

            for (final Servlet servlet : webModule.getWebApp().getServlet()) {
                final ServletInfo servletInfo = new ServletInfo();
                servletInfo.servletName = servlet.getServletName();
                servletInfo.servletClass = servlet.getServletClass();
                servletInfo.mappings = webModule.getWebApp().getServletMappings(servletInfo.servletName);
                for (final ParamValue pv : servlet.getInitParam()) {
                    final ParamValueInfo pvi = new ParamValueInfo();
                    pvi.name = pv.getParamName();
                    pvi.value = pv.getParamValue();
                    servletInfo.initParams.add(pvi);
                }
                webAppInfo.servlets.add(servletInfo);
            }

            for (final Listener listener : webModule.getWebApp().getListener()) {
                final ListenerInfo listenerInfo = new ListenerInfo();
                listenerInfo.classname = listener.getListenerClass();
                webAppInfo.listeners.add(listenerInfo);
            }

            for (final Filter filter : webModule.getWebApp().getFilter()) {
                final FilterInfo filterInfo = new FilterInfo();
                filterInfo.name = filter.getFilterName();
                filterInfo.classname = filter.getFilterClass();
                filterInfo.mappings = webModule.getWebApp().getFilterMappings(filter.getFilterName());
                for (final ParamValue pv : filter.getInitParam()) {
                    filterInfo.initParams.put(pv.getParamName(), pv.getParamValue());
                }
                webAppInfo.filters.add(filterInfo);
            }

            appInfo.webApps.add(webAppInfo);
        }
    }

    private void buildConnectorModules(final AppModule appModule, final AppInfo appInfo) throws OpenEJBException {
        final String appId = appModule.getModuleId();

        for (final ConnectorModule connectorModule : appModule.getConnectorModules()) {
            //
            // DEVELOPERS NOTE:  if you change the id generation code here, you must change
            // the id generation code in AutoConfig$AppResources
            //

            final Connector connector = connectorModule.getConnector();

            final ConnectorInfo connectorInfo = new ConnectorInfo();
            connectorInfo.description = connector.getDescription();
            connectorInfo.displayName = connector.getDisplayName();
            connectorInfo.path = connectorModule.getJarLocation();
            connectorInfo.moduleId = connectorModule.getModuleId();
            connectorInfo.watchedResources.addAll(connectorModule.getWatchedResources());
            connectorInfo.validationInfo = ValidatorBuilder.getInfo(connectorModule.getValidationConfig());
            connectorInfo.uniqueId = connectorModule.getUniqueId();
            connectorInfo.mbeans = connectorModule.getMbeans();

            final List<URL> libraries = connectorModule.getLibraries();
            for (final URL url : libraries) {
                final File file = toFile(url);
                try {
                    connectorInfo.libs.add(file.getCanonicalPath());
                } catch (final IOException e) {
                    throw new IllegalArgumentException("Invalid application lib path " + file.getAbsolutePath());
                }
            }

            final ResourceAdapter resourceAdapter = connector.getResourceAdapter();
            if (resourceAdapter.getResourceAdapterClass() != null) {
                final String id = this.getId(connectorModule);
                final String className = resourceAdapter.getResourceAdapterClass();

                final ServiceProvider provider = new ServiceProvider(className, id, "Resource");
                provider.getTypes().add(className);

                ServiceUtils.registerServiceProvider(appId, provider);

                final Resource resource = new Resource(id, className, appId + "#" + id);

                for (final ConfigProperty property : resourceAdapter.getConfigProperty()) {
                    final String name = property.getConfigPropertyName();
                    final String value = property.getConfigPropertyValue();
                    if (value != null) {
                        resource.getProperties().setProperty(name, value);
                    }
                }
                connectorInfo.resourceAdapter = this.configFactory.configureService(resource, ResourceInfo.class);
            }

            final OutboundResourceAdapter outbound = resourceAdapter.getOutboundResourceAdapter();
            if (outbound != null) {
                String transactionSupport = "none";
                final TransactionSupportType transactionSupportType = outbound.getTransactionSupport();
                if (transactionSupportType != null) {
                    switch (transactionSupportType) {
                        case LOCAL_TRANSACTION:
                            transactionSupport = "local";
                            break;
                        case NO_TRANSACTION:
                            transactionSupport = "none";
                            break;
                        case XA_TRANSACTION:
                            transactionSupport = "xa";
                            break;
                    }
                }
                for (final ConnectionDefinition connection : outbound.getConnectionDefinition()) {

                    final String id = this.getId(connection, outbound, connectorModule);
                    final String className = connection.getManagedConnectionFactoryClass();
                    final String type = connection.getConnectionFactoryInterface();

                    final ServiceProvider provider = new ServiceProvider(className, id, "Resource");
                    provider.getTypes().add(type);

                    ServiceUtils.registerServiceProvider(appId, provider);

                    final Resource resource = new Resource(id, type, appId + "#" + id);
                    final Properties properties = resource.getProperties();
                    for (final ConfigProperty property : connection.getConfigProperty()) {
                        final String name = property.getConfigPropertyName();
                        final String value = property.getConfigPropertyValue();
                        if (value != null) {
                            properties.setProperty(name, value);
                        }
                    }
                    properties.setProperty("TransactionSupport", transactionSupport);
                    if (connectorInfo.resourceAdapter != null) {
                        properties.setProperty("ResourceAdapter", connectorInfo.resourceAdapter.id);
                    }

                    final ResourceInfo resourceInfo = this.configFactory.configureService(resource, ResourceInfo.class);
                    connectorInfo.outbound.add(resourceInfo);
                }
            }

            final InboundResourceadapter inbound = resourceAdapter.getInboundResourceAdapter();
            if (inbound != null) {
                for (final MessageListener messageListener : inbound.getMessageAdapter().getMessageListener()) {
                    final String id = this.getId(messageListener, inbound, connectorModule);

                    final Container container = new Container(id, "MESSAGE", null);

                    final Properties properties = container.getProperties();
                    properties.setProperty("ResourceAdapter", connectorInfo.resourceAdapter.id);
                    properties.setProperty("MessageListenerInterface", messageListener.getMessageListenerType());
                    properties.setProperty("ActivationSpecClass", messageListener.getActivationSpec().getActivationSpecClass());

                    final MdbContainerInfo mdbContainerInfo = this.configFactory.configureService(container, MdbContainerInfo.class);
                    connectorInfo.inbound.add(mdbContainerInfo);
                }
            }

            for (final AdminObject adminObject : resourceAdapter.getAdminObject()) {

                final String id = this.getId(adminObject, resourceAdapter, connectorModule);
                final String className = adminObject.getAdminObjectClass();
                final String type = adminObject.getAdminObjectInterface();

                final ServiceProvider provider = new ServiceProvider(className, id, "Resource");
                provider.getTypes().add(type);

                ServiceUtils.registerServiceProvider(appId, provider);

                final Resource resource = new Resource(id, type, appId + "#" + id);
                final Properties properties = resource.getProperties();
                for (final ConfigProperty property : adminObject.getConfigProperty()) {
                    final String name = property.getConfigPropertyName();
                    final String value = property.getConfigPropertyValue();
                    if (value != null) {
                        properties.setProperty(name, value);
                    }
                }
                final ResourceInfo resourceInfo = this.configFactory.configureService(resource, ResourceInfo.class);
                connectorInfo.adminObject.add(resourceInfo);
            }

            appInfo.connectors.add(connectorInfo);
        }
    }

    private String getId(final AdminObject adminObject, final ResourceAdapter resourceAdapter, final ConnectorModule connectorModule) {
        final String id;
        if (adminObject.getId() != null) {
            id = adminObject.getId();
        } else if (resourceAdapter.getAdminObject().size() == 1) {
            id = connectorModule.getModuleId();
        } else {
            id = connectorModule.getModuleId() + "-" + adminObject.getAdminObjectInterface();
        }
        return id;
    }

    private String getId(final MessageListener messageListener, final InboundResourceadapter inbound, final ConnectorModule connectorModule) {
        final String id;
        if (messageListener.getId() != null) {
            id = messageListener.getId();
        } else if (inbound.getMessageAdapter().getMessageListener().size() == 1) {
            id = connectorModule.getModuleId();
        } else {
            id = connectorModule.getModuleId() + "-" + messageListener.getMessageListenerType();
        }
        return id;
    }

    private String getId(final ConnectionDefinition connection, final OutboundResourceAdapter outbound, final ConnectorModule connectorModule) {
        final String id;
        if (connection.getId() != null) {
            id = connection.getId();
        } else if (outbound.getConnectionDefinition().size() == 1) {
            id = connectorModule.getModuleId();
        } else {
            id = connectorModule.getModuleId() + "-" + connection.getConnectionFactoryInterface();
        }
        return id;
    }

    private String getId(final ConnectorModule connectorModule) {
        String id = connectorModule.getConnector().getResourceAdapter().getId();
        if (id == null) {
            id = connectorModule.getModuleId() + "RA";
        }
        return id;
    }

    private void buildPersistenceModules(final AppModule appModule, final AppInfo appInfo) {
        for (final PersistenceModule persistenceModule : appModule.getPersistenceModules()) {
            final String rootUrl = persistenceModule.getRootUrl();
            final Persistence persistence = persistenceModule.getPersistence();
            for (final PersistenceUnit persistenceUnit : persistence.getPersistenceUnit()) {
                final PersistenceUnitInfo info = new PersistenceUnitInfo();
                info.id = appModule.persistenceUnitId(rootUrl, persistenceUnit.getName());
                info.name = persistenceUnit.getName();
                info.watchedResources.addAll(persistenceModule.getWatchedResources());
                info.persistenceUnitRootUrl = rootUrl;
                info.provider = persistenceUnit.getProvider();
                info.transactionType = persistenceUnit.getTransactionType().toString();
                info.webappName = findRelatedWebApp(appModule, rootUrl);

                final Boolean excludeUnlistedClasses = persistenceUnit.isExcludeUnlistedClasses();
                info.excludeUnlistedClasses = persistenceUnit.isScanned() || excludeUnlistedClasses != null && excludeUnlistedClasses;

                info.jtaDataSource = persistenceUnit.getJtaDataSource();
                info.nonJtaDataSource = persistenceUnit.getNonJtaDataSource();

                info.jarFiles.addAll(persistenceUnit.getJarFile());
                info.classes.addAll(persistenceUnit.getClazz());
                info.mappingFiles.addAll(persistenceUnit.getMappingFile());

                info.persistenceXMLSchemaVersion = persistence.getVersion();
                info.sharedCacheMode = persistenceUnit.getSharedCacheMode().toString();
                info.validationMode = persistenceUnit.getValidationMode().toString();

                // Handle Properties
                info.properties.putAll(persistenceUnit.getProperties());

                PersistenceProviderProperties.apply(appModule, info);


                // Persistence Unit Root Url
                appInfo.persistenceUnits.add(info);
            }
        }
    }

    @SuppressWarnings("unchecked")
    private String findRelatedWebApp(final AppModule appModule, final String rootUrl) {
        for (final WebModule webModule : appModule.getWebModules()) {
            final List<URL> pXmls = (List<URL>) webModule.getAltDDs().get(DeploymentLoader.EAR_WEBAPP_PERSISTENCE_XML_JARS);
            if (pXmls != null) {
                for (final URL url : pXmls) {
                    if (url.toExternalForm().contains(rootUrl)) {
                        return webModule.getModuleId();
                    }
                }
            }
        }
        return null;
    }

    public static class PersistenceProviderProperties {
        public static final String OPENJPA_RUNTIME_UNENHANCED_CLASSES = "openjpa.RuntimeUnenhancedClasses";
        public static final String DEFAULT_RUNTIME_UNENHANCED_CLASSES = "supported";
        public static final String REMOVE_DEFAULT_RUNTIME_UNENHANCED_CLASSES = "disable";

        public static final String TABLE_PREFIX = "openejb.jpa.table_prefix";
        public static final String OPENJPA_METADATA_REPOSITORY = "openjpa.MetaDataRepository";
        public static final String PREFIX_METADATA_REPOSITORY = "org.apache.openejb.openjpa.PrefixMappingRepository";
        public static final String OPENJPA_SEQUENCE = "openjpa.Sequence";
        public static final String PREFIX_SEQUENCE = "org.apache.openejb.openjpa.PrefixTableJdbcSeq";

        public static final String PROVIDER_PROP = "javax.persistence.provider";
        public static final String TRANSACTIONTYPE_PROP = "javax.persistence.transactionType";
        public static final String JTADATASOURCE_PROP = "javax.persistence.jtaDataSource";
        public static final String NON_JTADATASOURCE_PROP = "javax.persistence.nonJtaDataSource";
        private static final String DEFAULT_PERSISTENCE_PROVIDER = PersistenceBootstrap.DEFAULT_PROVIDER;
        public static final String FORCE_PROVIDER_ENV = "openejb.jpa.force." + PROVIDER_PROP;

        public static final String HIBERNATE_TRANSACTION_MANAGER_LOOKUP_CLASS = "hibernate.transaction.manager_lookup_class";
        public static final String HIBERNATE_JTA_PLATFORM = "hibernate.transaction.jta.platform";
        public static final String HIBERNATE_EJB_NAMING_STRATEGY_PROP = "hibernate.ejb.naming_strategy";
        private static final String HIBERNATE_EJB_NAMING_STRATEGY = "org.apache.openejb.jpa.integration.hibernate.PrefixNamingStrategy";

        private static final String ECLIPSELINK_SESSION_CUSTOMIZER = "eclipselink.session.customizer";
        private static final String ECLIPSELINK_TARGET_SERVER = "eclipselink.target-server";
        private static final String PREFIX_SESSION_CUSTOMIZER = "org.apache.openejb.jpa.integration.eclipselink.PrefixSessionCustomizer";
        private static final String OPENEJB_TARGET_SERVER = "org.apache.openejb.jpa.integration.eclipselink.OpenEJBServerPlatform";

        private static String providerEnv;
        private static boolean forceProviderEnv;
        private static String transactionTypeEnv;
        private static String jtaDataSourceEnv;
        private static String nonJtaDataSourceEnv;

        static {
            providerEnv = SystemInstance.get().getOptions().get(PROVIDER_PROP, (String) null);
            forceProviderEnv = SystemInstance.get().getOptions().get(FORCE_PROVIDER_ENV, true);
            transactionTypeEnv = SystemInstance.get().getOptions().get(TRANSACTIONTYPE_PROP, (String) null);
            jtaDataSourceEnv = SystemInstance.get().getOptions().get(JTADATASOURCE_PROP, (String) null);
            nonJtaDataSourceEnv = SystemInstance.get().getOptions().get(NON_JTADATASOURCE_PROP, (String) null);
        }

        /**
         * @param appModule the app module with its config and its temp classloader, take care to only use getResource here
         * @param info        the persistence unit info
         */
        private static void apply(final AppModule appModule, final PersistenceUnitInfo info) {
            final ClassLoader classLoader = appModule.getClassLoader();
            overrideFromSystemProp(info);

            // The result is that OpenEJB-specific configuration can be avoided when
            // using OpenEJB + Hibernate or another vendor.  A second benefit is that
            // if another vendor is used in production, the value will automatically
            // be reset for using OpenEJB in the test environment.  Ensuring the strategy
            // doesn't start with "org.hibernate.transaction" allows for a custom lookup
            // strategy to be used and not overridden.

            // DMB: This whole block could be a map, but I left it this way just
            // in case we decided we wanted to do other custom handing for the
            // providers listed.
            if ("org.hibernate.ejb.HibernatePersistence".equals(info.provider) || "org.hibernate.jpa.HibernatePersistenceProvider".equals(info.provider)) {

                // Apply the overrides that apply to all persistence units of this provider
                override(appModule.getProperties(), info, "hibernate");

                String className = info.properties.getProperty(HIBERNATE_JTA_PLATFORM);
                if (className == null) {
                    className = info.properties.getProperty(HIBERNATE_TRANSACTION_MANAGER_LOOKUP_CLASS);
                }
                // info.persistenceUnitRootUrl = null; // to avoid HHH015010

                final String prefix = info.properties.getProperty(TABLE_PREFIX);
                if (prefix != null) {
                    if (info.properties.containsKey(HIBERNATE_EJB_NAMING_STRATEGY_PROP)) {
                        logger.warning("can't statisfy table prefix since you provided a " + HIBERNATE_EJB_NAMING_STRATEGY_PROP + " property");
                    } else {
                        // to pass the config to the impl
                        info.properties.setProperty(HIBERNATE_EJB_NAMING_STRATEGY_PROP, HIBERNATE_EJB_NAMING_STRATEGY);
                    }
                }

                if (className == null || className.startsWith("org.hibernate.transaction") || className.startsWith("org.hibernate.service.jta.platform")) {
                    // hibernate 4.3
                    String key = HIBERNATE_JTA_PLATFORM;
                    String value = MakeTxLookup.HIBERNATE_NEW_FACTORY2;

                    if (classLoader.getResource(ClassLoaderUtil.resourceName("org.hibernate.engine.transaction.jta.platform.spi.JtaPlatform")) == null) {
                        // previous hibernate 4
                        value = MakeTxLookup.HIBERNATE_NEW_FACTORY;

                        if (classLoader.getResource(ClassLoaderUtil.resourceName("org.hibernate.service.jta.platform.spi.JtaPlatform")) == null) {
                            // hibernate 3. In the worse case it is set with a hibernate 4 and hibernate will convert it.
                            key = HIBERNATE_TRANSACTION_MANAGER_LOOKUP_CLASS;
                            value = MakeTxLookup.HIBERNATE_FACTORY;
                        }
                    }

                    if (classLoader.getResource(ClassLoaderUtil.resourceName(value)) != null) {
                        info.properties.setProperty(key, value);
                        logger.debug("Adjusting PersistenceUnit(name=" + info.name + ") property to " + key + "=" + value);
                    } else {
                        logger.debug("can't adjust hibernate jta bridge to openejb one");
                    }

                }
            } else if ("oracle.toplink.essentials.PersistenceProvider".equals(info.provider) ||
                "oracle.toplink.essentials.ejb.cmp3.EntityManagerFactoryProvider".equals(info.provider)) {

                // Apply the overrides that apply to all persistence units of this provider
                override(appModule.getProperties(), info, "toplink");

                final String lookupProperty = "toplink.target-server";
                final String openejbLookupClass = MakeTxLookup.TOPLINK_FACTORY;

                final String prefix = info.properties.getProperty(TABLE_PREFIX);
                if (prefix != null) {
                    logger.warning("table prefix feature is not supported for toplink");
                }

                final String className = info.properties.getProperty(lookupProperty);

                if (className == null || className.startsWith("oracle.toplink.transaction")) {
                    info.properties.setProperty(lookupProperty, openejbLookupClass);
                    logger.debug("Adjusting PersistenceUnit(name=" + info.name + ") property to " + lookupProperty + "=" + openejbLookupClass);
                }
            } else if ("org.eclipse.persistence.jpa.PersistenceProvider".equals(info.provider) || "org.eclipse.persistence.jpa.osgi.PersistenceProvider".equals(info.provider)) {

                // Apply the overrides that apply to all persistence units of this provider
                override(appModule.getProperties(), info, "eclipselink");

                final String className = info.properties.getProperty(ECLIPSELINK_TARGET_SERVER);

                if (className == null || className.startsWith("org.eclipse.persistence.transaction")) {
                    if (classLoader.getResource(ClassLoaderUtil.resourceName(OPENEJB_TARGET_SERVER)) != null) {
                        info.properties.setProperty(ECLIPSELINK_TARGET_SERVER, OPENEJB_TARGET_SERVER);
                        logger.debug("Adjusting PersistenceUnit(name=" + info.name + ") property to " + ECLIPSELINK_TARGET_SERVER + "=" + OPENEJB_TARGET_SERVER);
                    } else {
                        logger.debug("Can't adjusting PersistenceUnit(name=" + info.name + ") property to " + ECLIPSELINK_TARGET_SERVER + "=" + OPENEJB_TARGET_SERVER + ", using default one");
                    }
                }

                final String prefix = info.properties.getProperty(TABLE_PREFIX);
                if (prefix != null) {
                    if (info.properties.containsKey(ECLIPSELINK_SESSION_CUSTOMIZER)) {
                        logger.warning("can't statisfy table prefix since you provided a " + ECLIPSELINK_SESSION_CUSTOMIZER + " property, add a call to org.apache.openejb.jpa.integration.eclipselink.PrefixSessionCustomizer");
                    } else {
                        // force eager loading otherwise we'll not get the prefix in the customizer
                        info.properties.setProperty(EntityManagerFactoryCallable.OPENEJB_JPA_INIT_ENTITYMANAGER, "true");
                        // to pass the config to the impl
                        info.properties.setProperty(ECLIPSELINK_SESSION_CUSTOMIZER, PREFIX_SESSION_CUSTOMIZER);
                    }
                }

                for (final String key : singletonList("eclipselink.jdbc.sequence-connection-pool.non-jta-data-source")) {
                    final String ds = info.properties.getProperty(key);
                    if (ds != null && !ds.contains(":") /* java:, openejb:, other: namespace */) {
                        info.properties.setProperty(key, "java:openejb/Resource/" + ds);
                    }
                }
            } else if (info.provider == null || "org.apache.openjpa.persistence.PersistenceProviderImpl".equals(info.provider)) {

                // Apply the overrides that apply to all persistence units of this provider
                override(appModule.getProperties(), info, "openjpa");

                final String existing = info.properties.getProperty(OPENJPA_RUNTIME_UNENHANCED_CLASSES);

                if (existing == null) {
                    info.properties.setProperty(OPENJPA_RUNTIME_UNENHANCED_CLASSES, DEFAULT_RUNTIME_UNENHANCED_CLASSES);
                    logger.debug("Adjusting PersistenceUnit(name=" + info.name + ") property to "
                        + OPENJPA_RUNTIME_UNENHANCED_CLASSES + "=" + DEFAULT_RUNTIME_UNENHANCED_CLASSES);
                } else if (REMOVE_DEFAULT_RUNTIME_UNENHANCED_CLASSES.equals(existing.trim())) {
                    info.properties.remove(OPENJPA_RUNTIME_UNENHANCED_CLASSES);
                    logger.info("Adjusting PersistenceUnit(name=" + info.name + ") removing property "
                        + OPENJPA_RUNTIME_UNENHANCED_CLASSES);
                }

                final String prefix = info.properties.getProperty(TABLE_PREFIX);
                if (prefix != null) {
                    final String mapping = info.properties.getProperty(OPENJPA_METADATA_REPOSITORY);
                    if (mapping != null && !"org.apache.openjpa.jdbc.meta.MappingRepository".equals(mapping)) {
                        throw new OpenEJBRuntimeException("can't honor table prefixes since you provided a custom mapping repository: " + mapping);
                    }
                    info.properties.setProperty(OPENJPA_METADATA_REPOSITORY, PREFIX_METADATA_REPOSITORY + "(prefix=" + prefix + ")");
                    if (!info.properties.containsKey(OPENJPA_SEQUENCE)) {
                        info.properties.setProperty(OPENJPA_SEQUENCE, PREFIX_SEQUENCE + "(prefix=" + prefix + ")");
                    } else {
                        logger.warning("you configured a custom sequence so the prefix will be ignored");
                    }
                }

                final Set<String> keys = new HashSet<String>(info.properties.stringPropertyNames());
                for (final String key : keys) {
                    if (key.matches("openjpa.Connection(DriverName|URL|UserName|Password)")) {
                        final Object o = info.properties.remove(key);
                        logger.warning("Removing PersistenceUnit(name=" + info.name + ") property " + key + "=" + o + "  [not valid in a container environment]");
                    } else { // try to convert it if necessary
                        final JPAPropertyConverter.Pair pair = JPAPropertyConverter.toOpenJPAValue(key, info.properties.getProperty(key), info.properties);
                        if (pair != null && !info.properties.containsKey(pair.getKey())) {
                            logger.info("Converting PersistenceUnit(name=" + info.name + ") property "
                                + key + "=" + info.properties.getProperty(key) + " to " + pair.toString());
                            info.properties.remove(key);
                            info.properties.setProperty(pair.getKey(), pair.getValue());
                        }
                    }
                }
            }

            // Apply the overrides that apply to just this persistence unit
            override(appModule.getProperties(), info);

            for (final String key : asList("javax.persistence.jtaDataSource", "javax.persistence.nonJtaDataSource")) {
                final String ds = info.properties.getProperty(key);
                if (ds != null && !ds.contains(":") /* java:, openejb:, other: namespace */) {
                    info.properties.setProperty(key, "java:openejb/Resource/" + ds);
                }
            }
        }

        private static void overrideFromSystemProp(final PersistenceUnitInfo info) {
            if (providerEnv != null && (info.provider == null || forceProviderEnv)) {
                info.provider = providerEnv;
            }
            if (info.provider == null) {
                info.provider = DEFAULT_PERSISTENCE_PROVIDER;
            }
            if (jtaDataSourceEnv != null) {
                info.jtaDataSource = jtaDataSourceEnv;
            }
            if (transactionTypeEnv != null) {
                info.transactionType = transactionTypeEnv.toUpperCase();
            }
            if (nonJtaDataSourceEnv != null) {
                info.nonJtaDataSource = nonJtaDataSourceEnv;
            }
        }

        private static void override(final Properties appProperties, final PersistenceUnitInfo info) {
            override(appProperties, info, info.name);
        }

        private static void override(final Properties appProperties, final PersistenceUnitInfo info, final String prefix) {

            final Properties propertiesToCheckForOverridings = new Properties(System.getProperties());
            propertiesToCheckForOverridings.putAll(SystemInstance.get().getProperties());
            propertiesToCheckForOverridings.putAll(appProperties);
            final Properties overrides = ConfigurationFactory.getOverrides(propertiesToCheckForOverridings, prefix, "PersistenceUnit");

            for (final Map.Entry<Object, Object> entry : overrides.entrySet()) {
                final String property = (String) (prefix.equalsIgnoreCase(info.name) ? entry.getKey() : prefix + "." + entry.getKey());
                String value = (String) entry.getValue();

                if ("openjpa.Log".equals(property) && "org.apache.openejb.openjpa.JULOpenJPALogFactory".equals(value)) { // we set a default
                    if (info.properties.containsKey("openjpa.Log")) {
                        continue;
                    }
                    if (appProperties.containsKey("openjpa.Log")) {
                        value = appProperties.getProperty(property, value);
                    } else {
                        continue;
                    }
                }

                if (info.properties.containsKey(property)) {
                    logger.debug("Overriding persistence-unit " + info.name + " property " + property + "=" + value);
                } else {
                    logger.debug("Adding persistence-unit " + info.name + " property " + property + "=" + value);
                }
                info.properties.put(property, value);

                if (property.endsWith("openjpa.Specification")) {
                    info.persistenceXMLSchemaVersion = value.replace("JPA ", "");
                }
            }
        }
    }

    private static String getClientModuleId(final ClientModule clientModule) {
        return clientModule.getModuleId();
    }


    private List<PortInfo> configureWebservices(final Webservices webservices) {
        final List<PortInfo> portMap = new ArrayList<PortInfo>();
        if (webservices == null) {
            return portMap;
        }

        for (final WebserviceDescription desc : webservices.getWebserviceDescription()) {
            final String wsdlFile = desc.getWsdlFile();
            final String serviceName = desc.getWebserviceDescriptionName();

            for (final PortComponent port : desc.getPortComponent()) {
                final PortInfo portInfo = new PortInfo();

                final ServiceImplBean serviceImplBean = port.getServiceImplBean();
                portInfo.serviceId = desc.getId();
                portInfo.portId = port.getId();
                portInfo.serviceLink = serviceImplBean.getEjbLink();
                if (portInfo.serviceLink == null) {
                    portInfo.serviceLink = serviceImplBean.getServletLink();
                }

                portInfo.seiInterfaceName = port.getServiceEndpointInterface();
                portInfo.portName = port.getPortComponentName();
                portInfo.binding = port.getProtocolBinding();
                portInfo.serviceName = serviceName;
                portInfo.wsdlFile = wsdlFile;
                portInfo.mtomEnabled = port.isEnableMtom();
                portInfo.wsdlPort = port.getWsdlPort();
                portInfo.wsdlService = port.getWsdlService();
                portInfo.location = port.getLocation();

                final List<HandlerChainInfo> handlerChains = ConfigurationFactory.toHandlerChainInfo(port.getHandlerChains());
                portInfo.handlerChains.addAll(handlerChains);

                // todo configure jaxrpc mappings here

                portMap.add(portInfo);
            }
        }
        return portMap;
    }

    /*
     * left package-local for a unit test
     */
    void configureWebserviceSecurity(final EjbJarInfo ejbJarInfo, final EjbModule ejbModule) {
        final Object altDD = ejbModule.getOpenejbJar();
        final List<PortInfo> infoList = ejbJarInfo.portInfos;

        this.configureWebserviceScurity(infoList, altDD);
    }

    private void configureWebserviceScurity(final List<PortInfo> infoList, final Object altDD) {
        if (altDD == null || !(altDD instanceof OpenejbJar)) {
            return;
        }

        final OpenejbJar openejbJar = (OpenejbJar) altDD;
        final Map<String, EjbDeployment> deploymentsByEjbName = openejbJar.getDeploymentsByEjbName();

        for (final PortInfo portInfo : infoList) {
            final EjbDeployment deployment = deploymentsByEjbName.get(portInfo.serviceLink);

            if (deployment == null) {
                continue;
            }
            portInfo.realmName = deployment.getProperties().getProperty("webservice.security.realm");
            portInfo.securityRealmName = deployment.getProperties().getProperty("webservice.security.securityRealm");
            if (deployment.getProperties().getProperty("webservice.security.transportGarantee") != null) {
                portInfo.transportGuarantee = deployment.getProperties().getProperty("webservice.security.transportGarantee");
            } else {
                portInfo.transportGuarantee = "NONE";
            }

            if (deployment.getProperties().getProperty("webservice.security.authMethod") != null) {
                portInfo.authMethod = deployment.getProperties().getProperty("webservice.security.authMethod");
            } else {
                portInfo.authMethod = "NONE";
            }
            portInfo.properties = deployment.getProperties();
        }
    }

    private static boolean skipMdb(final EnterpriseBeanInfo bean) {
        return bean instanceof MessageDrivenBeanInfo && SystemInstance.get().hasProperty("openejb.geronimo");
    }

}
