/*
 * JBoss, Home of Professional Open Source
 * Copyright (c) 2010, JBoss Inc., and individual contributors as indicated
 * by the @authors tag. See the copyright.txt in the distribution for a
 * full listing of individual contributors.
 *
 * This is free software; you can redistribute it and/or modify it
 * under the terms of the GNU Lesser General Public License as
 * published by the Free Software Foundation; either version 2.1 of
 * the License, or (at your option) any later version.
 *
 * This software is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this software; if not, write to the Free
 * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
 * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
 */
package org.jboss.as.ejb3.tx;

import static org.jboss.as.ejb3.tx.util.StatusHelper.statusAsString;

import java.lang.reflect.UndeclaredThrowableException;
import java.rmi.RemoteException;

import javax.ejb.EJBException;
import javax.ejb.EJBTransactionRolledbackException;
import javax.ejb.NoSuchEJBException;
import javax.ejb.NoSuchEntityException;
import javax.ejb.TransactionAttributeType;
import javax.transaction.HeuristicMixedException;
import javax.transaction.HeuristicRollbackException;
import javax.transaction.RollbackException;
import javax.transaction.Status;
import javax.transaction.SystemException;
import javax.transaction.Transaction;

import org.jboss.as.ee.component.Component;
import org.jboss.as.ejb3.logging.EjbLogger;
import org.jboss.as.ejb3.component.EJBComponent;
import org.jboss.as.ejb3.component.MethodIntf;
import org.jboss.as.ejb3.component.MethodIntfHelper;
import org.jboss.invocation.ImmediateInterceptorFactory;
import org.jboss.invocation.Interceptor;
import org.jboss.invocation.InterceptorContext;
import org.jboss.invocation.InterceptorFactory;
import org.wildfly.transaction.client.AbstractTransaction;
import org.wildfly.transaction.client.ContextTransactionManager;

/**
 * Ensure the correct exceptions are thrown based on both caller
 * transactional context and supported Transaction Attribute Type
 *

 * EJB3 13.6.2.6
 * EJB3 Core Specification 14.3.1 Table 14
 *
 * @author ALR
 * @author Carlo de Wolf
 * @author Scott Marlow
 */
public class CMTTxInterceptor implements Interceptor {

    public static final InterceptorFactory FACTORY = new ImmediateInterceptorFactory(new CMTTxInterceptor());

    /**
     * The endTransaction method ends a transaction and
     * translates any exceptions into
     * TransactionRolledBack[Local]Exception or SystemException.
     *
     * @param tx a Transaction value
     */
    protected void endTransaction(final Transaction tx) {
        ContextTransactionManager tm = ContextTransactionManager.getInstance();
        try {
            if (! tx.equals(tm.getTransaction())) {
                throw EjbLogger.ROOT_LOGGER.wrongTxOnThread(tx, tm.getTransaction());
            }
            final int txStatus = tx.getStatus();
            if (txStatus == Status.STATUS_ACTIVE) {
                // Commit tx
                // This will happen if
                // a) everything goes well
                // b) app. exception was thrown
                tm.commit();
            } else if (txStatus == Status.STATUS_MARKED_ROLLBACK) {
                tm.rollback();
            } else if (txStatus == Status.STATUS_ROLLEDBACK || txStatus == Status.STATUS_ROLLING_BACK) {
                // handle reaper canceled (rolled back) tx case (see WFLY-1346)
                // clear current tx state and throw RollbackException (EJBTransactionRolledbackException)
                tm.rollback();
                throw EjbLogger.ROOT_LOGGER.transactionAlreadyRolledBack(tx);
            } else if (txStatus == Status.STATUS_UNKNOWN) {
                // STATUS_UNKNOWN isn't expected to be reached here but if it does, we need to clear current thread tx.
                // It is possible that calling tm.commit() could succeed but we call tm.rollback, since this is an unexpected
                // tx state that are are handling.
                tm.rollback();
                // if the tm.rollback doesn't fail, we throw an EJBException to reflect the unexpected tx state.
                throw EjbLogger.ROOT_LOGGER.transactionInUnexpectedState(tx, statusAsString(txStatus));
            } else {
                // logically, all of the following (unexpected) tx states are handled here:
                //  Status.STATUS_PREPARED
                //  Status.STATUS_PREPARING
                //  Status.STATUS_COMMITTING
                //  Status.STATUS_NO_TRANSACTION
                //  Status.STATUS_COMMITTED
                tm.suspend();                       // clear current tx state and throw EJBException
                throw EjbLogger.ROOT_LOGGER.transactionInUnexpectedState(tx, statusAsString(txStatus));
            }
        } catch (RollbackException e) {
            throw new EJBTransactionRolledbackException(e.toString(), e);
        } catch (HeuristicMixedException | SystemException | HeuristicRollbackException e) {
            throw new EJBException(e);
        }
    }

    private void endTransaction(final Transaction tx, final Throwable outerEx) {
        try {
            endTransaction(tx);
        } catch (Throwable t) {
            outerEx.addSuppressed(t);
        }
    }

    public Object processInvocation(InterceptorContext invocation) throws Exception {
        final EJBComponent component = (EJBComponent) invocation.getPrivateData(Component.class);
        final ContextTransactionManager tm = ContextTransactionManager.getInstance();
        final int oldTimeout = tm.getTransactionTimeout();
        try {
            final MethodIntf methodIntf = MethodIntfHelper.of(invocation);
            final TransactionAttributeType attr = component.getTransactionAttributeType(methodIntf, invocation.getMethod());
            final int timeoutInSeconds = component.getTransactionTimeout(methodIntf, invocation.getMethod());
            switch (attr) {
                case MANDATORY:
                    return mandatory(invocation, component);
                case NEVER:
                    return never(invocation, component);
                case NOT_SUPPORTED:
                    return notSupported(invocation, component);
                case REQUIRED:
                    return required(invocation, component, timeoutInSeconds);
                case REQUIRES_NEW:
                    return requiresNew(invocation, component, timeoutInSeconds);
                case SUPPORTS:
                    return supports(invocation, component);
                default:
                    throw EjbLogger.ROOT_LOGGER.unknownTxAttributeOnInvocation(attr, invocation);
            }
        } finally {
            // See also https://issues.jboss.org/browse/WFTC-44
            tm.setTransactionTimeout(oldTimeout == ContextTransactionManager.getGlobalDefaultTransactionTimeout() ? 0 : oldTimeout);
        }
    }

    protected Object invokeInImportedTx(InterceptorContext invocation, EJBComponent component) throws Exception {
        Transaction tx;
        try {
            tx = invocation.getTransaction();
        } catch (SystemException ex) {
            // SystemException + server suspended equals the transaction is new and the request
            // for new transaction is being rejected
            if (component != null && component.getEjbSuspendHandlerService().isSuspended()) {
                throw EjbLogger.ROOT_LOGGER.cannotBeginUserTransaction();
            } else {
                throw new EJBException(ex);
            }
        }
        safeResume(tx);
        final Object result;
        try {
            result = invokeInCallerTx(invocation, tx, component);
        } catch (Throwable t) {
            safeSuspend(t);
            throw t;
        }
        safeSuspend();
        return result;
    }

    protected Object invokeInCallerTx(InterceptorContext invocation, Transaction tx, final EJBComponent component) throws Exception {
        try {
            return invocation.proceed();
        } catch (Error e) {
            final EJBTransactionRolledbackException e2 = EjbLogger.ROOT_LOGGER.unexpectedErrorRolledBack(e);
            setRollbackOnly(tx, e2);
            throw e2;
        } catch (Exception e) {
            ApplicationExceptionDetails ae = component.getApplicationException(e.getClass(), invocation.getMethod());

            if (ae != null) {
                if (ae.isRollback()) setRollbackOnly(tx, e);
                throw e;
            }
            try {
                throw e;
            } catch (EJBTransactionRolledbackException | NoSuchEJBException | NoSuchEntityException e2) {
                setRollbackOnly(tx, e2);
                throw e2;
            } catch (RuntimeException e2) {
                final EJBTransactionRolledbackException e3 = new EJBTransactionRolledbackException(e2.getMessage(), e2);
                setRollbackOnly(tx, e3);
                throw e3;
            }
        } catch (Throwable t) {
            final EJBException e = new EJBException(new UndeclaredThrowableException(t));
            setRollbackOnly(tx, e);
            throw e;
        }
    }

    protected Object invokeInNoTx(InterceptorContext invocation, final EJBComponent component) throws Exception {
        try {
            return invocation.proceed();
        } catch (Error e) {
            throw EjbLogger.ROOT_LOGGER.unexpectedError(e);
        } catch (EJBException e) {
            throw e;
        } catch (RuntimeException e) {
            ApplicationExceptionDetails ae = component.getApplicationException(e.getClass(), invocation.getMethod());
            throw ae != null ? e : new EJBException(e);
        } catch (Exception e) {
            throw e;
        } catch (Throwable t) {
            throw new EJBException(new UndeclaredThrowableException(t));
        }
    }

    protected Object invokeInOurTx(InterceptorContext invocation, final EJBComponent component) throws Exception {
        final ContextTransactionManager tm = ContextTransactionManager.getInstance();
        tm.begin();
        final AbstractTransaction tx = tm.getTransaction();
        final Object result;
        try {
            result = invocation.proceed();
        } catch (Throwable t) {
            ApplicationExceptionDetails ae = component.getApplicationException(t.getClass(), invocation.getMethod());
            try {
                try {
                    throw t;
                } catch (EJBException | RemoteException e) {
                    throw e;
                } catch (RuntimeException e) {
                    throw ae != null ? e : new EJBException(e);
                } catch (Exception e) {
                    throw e;
                } catch (Error e) {
                    throw EjbLogger.ROOT_LOGGER.unexpectedError(e);
                } catch (Throwable e) {
                    throw new EJBException(new UndeclaredThrowableException(e));
                }
            } catch (Throwable t2) {
                if (ae == null || ae.isRollback()) setRollbackOnly(tx, t2);
                endTransaction(tx, t2);
                throw t2;
            }
        }
        boolean rolledBack = safeGetStatus(tx) == Status.STATUS_MARKED_ROLLBACK;
        endTransaction(tx);
        if (rolledBack) ourTxRolledBack();
        return result;
    }

    private int safeGetStatus(final AbstractTransaction tx) {
        try {
            return tx.getStatus();
        } catch (SystemException e) {
            return Status.STATUS_UNKNOWN;
        }
    }

    private void safeSuspend() {
        try {
            ContextTransactionManager.getInstance().suspend();
        } catch (SystemException e) {
            throw new EJBException(e);
        }
    }

    private void safeSuspend(final Throwable t) {
        try {
            ContextTransactionManager.getInstance().suspend();
        } catch (SystemException e) {
            t.addSuppressed(e);
        }
    }

    private void safeResume(final Transaction tx) {
        try {
            ContextTransactionManager.getInstance().resume(tx);
        } catch (Exception e) {
            throw new EJBException(e);
        }
    }

    private void safeResume(final Transaction tx, final Throwable t) {
        try {
            ContextTransactionManager.getInstance().resume(tx);
        } catch (Exception e) {
            t.addSuppressed(e);
        }
    }

    protected void ourTxRolledBack() {
        // normally no operation
    }

    protected Object mandatory(InterceptorContext invocation, final EJBComponent component) throws Exception {
        final ContextTransactionManager tm = ContextTransactionManager.getInstance();
        Transaction tx = tm.getTransaction();
        if (tx == null) {
            if (invocation.hasTransaction()) {
                return invokeInImportedTx(invocation, component);
            }
            throw EjbLogger.ROOT_LOGGER.txRequiredForInvocation(invocation);
        }
        return invokeInCallerTx(invocation, tx, component);
    }

    protected Object never(InterceptorContext invocation, final EJBComponent component) throws Exception {
        final ContextTransactionManager tm = ContextTransactionManager.getInstance();
        if (tm.getTransaction() != null || invocation.hasTransaction()) {
            throw EjbLogger.ROOT_LOGGER.txPresentForNeverTxAttribute();
        }
        return invokeInNoTx(invocation, component);
    }

    protected Object notSupported(InterceptorContext invocation, final EJBComponent component) throws Exception {
        final ContextTransactionManager tm = ContextTransactionManager.getInstance();
        Transaction tx = tm.getTransaction();
        if (tx != null) {
            safeSuspend();
            final Object result;
            try {
                result = invokeInNoTx(invocation, component);
            } catch (Throwable t) {
                safeResume(tx, t);
                throw t;
            }
            safeResume(tx);
            return result;
        } else {
            return invokeInNoTx(invocation, component);
        }
    }

    protected Object required(final InterceptorContext invocation, final EJBComponent component, final int timeout) throws Exception {
        final ContextTransactionManager tm = ContextTransactionManager.getInstance();

        if (timeout != -1) {
            tm.setTransactionTimeout(timeout);
        }

        final Transaction tx = tm.getTransaction();

        if (tx == null) {
            if (invocation.hasTransaction()) {
                return invokeInImportedTx(invocation, component);
            }
            return invokeInOurTx(invocation, component);
        } else {
            return invokeInCallerTx(invocation, tx, component);
        }
    }

    protected Object requiresNew(InterceptorContext invocation, final EJBComponent component, final int timeout) throws Exception {
        final ContextTransactionManager tm = ContextTransactionManager.getInstance();

        if (timeout != -1) {
            tm.setTransactionTimeout(timeout);
        }

        Transaction tx = tm.getTransaction();
        if (tx != null) {
            safeSuspend();
            final Object result;
            try {
                result = invokeInOurTx(invocation, component);
            } catch (Throwable t) {
                safeResume(tx, t);
                throw t;
            }
            safeResume(tx);
            return result;
        } else {
            return invokeInOurTx(invocation, component);
        }
    }

    /**
     * The setRollbackOnly method calls setRollbackOnly()
     * on the invocation's transaction and logs any exceptions than may
     * occur.
     *
     * @param tx the transaction
     * @param t the exception to add problems to (may be {@code null})
     */
    protected void setRollbackOnly(Transaction tx, final Throwable t) {
        try {
            tx.setRollbackOnly();
        } catch (Throwable t2) {
            EjbLogger.ROOT_LOGGER.failedToSetRollbackOnly(t2);
            if (t != null) {
                t.addSuppressed(t2);
            }
        }
    }

    protected Object supports(InterceptorContext invocation, final EJBComponent component) throws Exception {
        final ContextTransactionManager tm = ContextTransactionManager.getInstance();
        Transaction tx = tm.getTransaction();
        if (tx == null) {
            if (invocation.hasTransaction()) {
                return invokeInImportedTx(invocation, component);
            }
            return invokeInNoTx(invocation, component);
        } else {
            return invokeInCallerTx(invocation, tx, component);
        }
    }
}


/*
 * JBoss, Home of Professional Open Source.
 * Copyright 2013, Red Hat, Inc., and individual contributors
 * as indicated by the @author tags. See the copyright.txt file in the
 * distribution for a full listing of individual contributors.
 *
 * This is free software; you can redistribute it and/or modify it
 * under the terms of the GNU Lesser General Public License as
 * published by the Free Software Foundation; either version 2.1 of
 * the License, or (at your option) any later version.
 *
 * This software is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this software; if not, write to the Free
 * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
 * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
 */

package org.wildfly.mod_cluster.undertow;

import java.util.Set;

import org.jboss.as.server.deployment.Attachments;
import org.jboss.as.server.deployment.DeploymentPhaseContext;
import org.jboss.as.server.deployment.DeploymentUnit;
import org.jboss.as.server.deployment.DeploymentUnitProcessor;
import org.jboss.modcluster.load.metric.LoadMetric;
import org.jboss.modcluster.load.metric.impl.BusyConnectorsLoadMetric;
import org.jboss.modcluster.load.metric.impl.ReceiveTrafficLoadMetric;
import org.jboss.modcluster.load.metric.impl.RequestCountLoadMetric;
import org.jboss.modcluster.load.metric.impl.SendTrafficLoadMetric;
import org.wildfly.extension.undertow.deployment.UndertowAttachments;
import org.wildfly.mod_cluster.undertow.metric.BytesReceivedHttpHandler;
import org.wildfly.mod_cluster.undertow.metric.BytesSentHttpHandler;
import org.wildfly.mod_cluster.undertow.metric.RequestCountHttpHandler;
import org.wildfly.mod_cluster.undertow.metric.RunningRequestsHttpHandler;

/**
 * {@link DeploymentUnitProcessor} which adds a dependency on {@link UndertowEventHandlerAdapterServiceConfigurator}s to web
 * dependencies to support session draining (see WFLY-3942) and
 * registers metrics on deployment if mod_cluster module is loaded.
 *

 *
{@link org.wildfly.mod_cluster.undertow.metric.RequestCountHttpHandler}

 *
{@link org.wildfly.mod_cluster.undertow.metric.RunningRequestsHttpHandler}

 *
{@link org.wildfly.mod_cluster.undertow.metric.BytesReceivedHttpHandler}

 *
{@link org.wildfly.mod_cluster.undertow.metric.BytesSentHttpHandler}

 *

 *
 * @author Radoslav Husar
 * @since 8.0
 */
public class ModClusterUndertowDeploymentProcessor implements DeploymentUnitProcessor {

    private final Set<String> adapterNames;
    private final Set<LoadMetric> enabledMetrics;

    public ModClusterUndertowDeploymentProcessor(Set<String> adapterNames, Set<LoadMetric> enabledMetrics) {
        this.adapterNames = adapterNames;
        this.enabledMetrics = enabledMetrics;
    }

    @Override
    public void deploy(DeploymentPhaseContext phaseContext) {
        DeploymentUnit deploymentUnit = phaseContext.getDeploymentUnit();

        // Add mod_cluster-undertow integration service as a web deployment dependency
        for (String adapter : adapterNames) {
            deploymentUnit.addToAttachmentList(Attachments.WEB_DEPENDENCIES, new UndertowEventHandlerAdapterServiceNameProvider(adapter).getServiceName());
        }

        // Request count wrapping
        if (isMetricEnabled(RequestCountLoadMetric.class)) {
            deploymentUnit.addToAttachmentList(UndertowAttachments.UNDERTOW_INITIAL_HANDLER_CHAIN_WRAPPERS, RequestCountHttpHandler::new);
        }

        // Bytes Sent wrapping
        if (isMetricEnabled(SendTrafficLoadMetric.class)) {
            deploymentUnit.addToAttachmentList(UndertowAttachments.UNDERTOW_INITIAL_HANDLER_CHAIN_WRAPPERS, BytesSentHttpHandler::new);
        }

        // Bytes Received wrapping
        if (isMetricEnabled(ReceiveTrafficLoadMetric.class)) {
            deploymentUnit.addToAttachmentList(UndertowAttachments.UNDERTOW_INITIAL_HANDLER_CHAIN_WRAPPERS, BytesReceivedHttpHandler::new);
        }

        // Busyness thread setup actions
        if (isMetricEnabled(BusyConnectorsLoadMetric.class)) {
            deploymentUnit.addToAttachmentList(UndertowAttachments.UNDERTOW_OUTER_HANDLER_CHAIN_WRAPPERS, RunningRequestsHttpHandler::new);
        }

    }

    @Override
    public void undeploy(DeploymentUnit context) {
        // Do nothing.
    }

    /**
     * Checks whether this {@link Class} is configured to be used.
     *
     * @param metricClass Class to check whether it's configured to be used
     * @return true if any of the enabled metrics is enabled, false otherwise
     */
    private boolean isMetricEnabled(Class metricClass) {
        for (LoadMetric enabledMetric : enabledMetrics) {
            if (metricClass.isInstance(enabledMetric)) {
                return true;
            }
        }

        return false;
    }

}


/*
 * JBoss, Home of Professional Open Source.
 * Copyright 2011, Red Hat, Inc., and individual contributors
 * as indicated by the @author tags. See the copyright.txt file in the
 * distribution for a full listing of individual contributors.
 *
 * This is free software; you can redistribute it and/or modify it
 * under the terms of the GNU Lesser General Public License as
 * published by the Free Software Foundation; either version 2.1 of
 * the License, or (at your option) any later version.
 *
 * This software is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this software; if not, write to the Free
 * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
 * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
 */

package org.jboss.as.webservices.injection;

import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;

/**
 * Defines mapping of Jaxws endpoints and their handlers.
 *
 * @author Richard Opalka
 */
public final class WSEndpointHandlersMapping {

    private final Map<String, Set<String>> endpointHandlersMap = new HashMap<String, Set<String>>();

    /**
     * Registers endpoint and its associated WS handlers.
     *
     * @param endpointClass WS endpoint
     * @param endpointHandlers WS handlers associated with endpoint
     */
    public void registerEndpointHandlers(final String endpointClass, final Set<String> endpointHandlers) {
        if ((endpointClass == null) || (endpointHandlers == null)) {
            throw new IllegalArgumentException();
        }
        endpointHandlersMap.put(endpointClass, Collections.unmodifiableSet(endpointHandlers));
    }

    /**
     * Returns handlers class names associated with WS endpoint.
     *
     * @param endpointClass to get associated handlers for
     * @return associated handlers class names
     */
    public Set<String> getHandlers(final String endpointClass) {
        return endpointHandlersMap.get(endpointClass);
    }

    public boolean isEmpty() {
        return endpointHandlersMap.size() == 0;
    }

}


/*
 * JBoss, Home of Professional Open Source.
 * Copyright 2017, Red Hat, Inc., and individual contributors
 * as indicated by the @author tags. See the copyright.txt file in the
 * distribution for a full listing of individual contributors.
 *
 * This is free software; you can redistribute it and/or modify it
 * under the terms of the GNU Lesser General Public License as
 * published by the Free Software Foundation; either version 2.1 of
 * the License, or (at your option) any later version.
 *
 * This software is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this software; if not, write to the Free
 * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
 * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
 */

package org.jboss.as.ejb3.subsystem;

import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.ADD;
import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.NAME;
import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.OPERATION_HEADERS;
import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.READ_ATTRIBUTE_OPERATION;
import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.UNDEFINE_ATTRIBUTE_OPERATION;
import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.WRITE_ATTRIBUTE_OPERATION;
import static org.jboss.as.ejb3.subsystem.EJB3SubsystemModel.ALLOW_EXECUTION;
import static org.jboss.as.ejb3.subsystem.EJB3SubsystemModel.CLIENT_MAPPINGS_CLUSTER_NAME;
import static org.jboss.as.ejb3.subsystem.EJB3SubsystemModel.DEFAULT_SFSB_CACHE;
import static org.jboss.as.ejb3.subsystem.EJB3SubsystemModel.DEFAULT_SFSB_PASSIVATION_DISABLED_CACHE;
import static org.jboss.as.ejb3.subsystem.EJB3SubsystemModel.EXECUTE_IN_WORKER;
import static org.jboss.as.ejb3.subsystem.EJB3SubsystemModel.REFRESH_INTERVAL;
import static org.jboss.as.ejb3.subsystem.StrictMaxPoolResourceDefinition.DERIVE_SIZE;

import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;

import org.jboss.as.clustering.controller.Operations;
import org.jboss.as.controller.ModelVersion;
import org.jboss.as.controller.OperationFailedException;
import org.jboss.as.controller.PathAddress;
import org.jboss.as.controller.PathElement;
import org.jboss.as.controller.descriptions.ModelDescriptionConstants;
import org.jboss.as.controller.registry.Resource;
import org.jboss.as.controller.transform.CombinedTransformer;
import org.jboss.as.controller.transform.ExtensionTransformerRegistration;
import org.jboss.as.controller.transform.OperationRejectionPolicy;
import org.jboss.as.controller.transform.OperationResultTransformer;
import org.jboss.as.controller.transform.OperationTransformer;
import org.jboss.as.controller.transform.ResourceTransformationContext;
import org.jboss.as.controller.transform.SubsystemTransformerRegistration;
import org.jboss.as.controller.transform.TransformationContext;
import org.jboss.as.controller.transform.description.AttributeConverter;
import org.jboss.as.controller.transform.description.DiscardAttributeChecker;
import org.jboss.as.controller.transform.description.RejectAttributeChecker;
import org.jboss.as.controller.transform.description.ResourceTransformationDescriptionBuilder;
import org.jboss.as.controller.transform.description.TransformationDescription;
import org.jboss.as.controller.transform.description.TransformationDescriptionBuilder;
import org.jboss.as.ejb3.logging.EjbLogger;
import org.jboss.dmr.ModelNode;
import org.wildfly.clustering.ejb.BeanManagerFactoryServiceConfiguratorConfiguration;

/**
 * @author Tomaz Cerar (c) 2017 Red Hat Inc.
 */
public class EJBTransformers implements ExtensionTransformerRegistration {
    private static final ModelVersion VERSION_1_2_1 = ModelVersion.create(1, 2, 1);
    private static final ModelVersion VERSION_1_3_0 = ModelVersion.create(1, 3, 0);
    private static final ModelVersion VERSION_3_0_0 = ModelVersion.create(3, 0, 0);
    private static final ModelVersion VERSION_4_0_0 = ModelVersion.create(4, 0, 0);

    @Override
    public String getSubsystemName() {
        return EJB3Extension.SUBSYSTEM_NAME;
    }

    @Override
    public void registerTransformers(SubsystemTransformerRegistration subsystemRegistration) {
        registerTransformers_1_2_1(subsystemRegistration);
        registerTimerTransformers_1_3_0(subsystemRegistration);
        registerTransformers_3_0_0(subsystemRegistration);
        registerTransformers_4_0_0(subsystemRegistration);
    }

    private static void registerTransformers_1_2_1(SubsystemTransformerRegistration subsystemRegistration) {
        registerTransformers_1_2_1_and_1_3_0(subsystemRegistration, VERSION_1_2_1);
    }

    private static void registerTimerTransformers_1_3_0(SubsystemTransformerRegistration subsystemRegistration) {
        registerTransformers_1_2_1_and_1_3_0(subsystemRegistration, VERSION_1_3_0);
    }

    private static void registerTransformers_1_2_1_and_1_3_0(SubsystemTransformerRegistration subsystemRegistration, ModelVersion version) {
        final ResourceTransformationDescriptionBuilder builder = TransformationDescriptionBuilder.Factory.createSubsystemInstance();

        StatefulCacheRefTransformer statefulCacheRefTransformer = new StatefulCacheRefTransformer();
        builder.setCustomResourceTransformer(statefulCacheRefTransformer);

        for (String name : Arrays.asList(WRITE_ATTRIBUTE_OPERATION, UNDEFINE_ATTRIBUTE_OPERATION, READ_ATTRIBUTE_OPERATION)) {
            builder.addOperationTransformationOverride(name)
                    .inheritResourceAttributeDefinitions()
                    .setCustomOperationTransformer(statefulCacheRefTransformer)
                    .end();
        }

        builder.addOperationTransformationOverride(ADD)
                .inheritResourceAttributeDefinitions()
                .setCustomOperationTransformer(new AddStatefulCacheRefTransformer())
                .end();

        //This used to behave as 'true' and it is now defaulting as 'true'
        builder.getAttributeBuilder().setDiscard(new DiscardAttributeChecker.DiscardAttributeValueChecker(new ModelNode(true)), EJB3SubsystemRootResourceDefinition.LOG_EJB_EXCEPTIONS);
        builder.getAttributeBuilder().addRejectCheck(RejectAttributeChecker.DEFINED, EJB3SubsystemRootResourceDefinition.LOG_EJB_EXCEPTIONS);

        builder.getAttributeBuilder().addRejectCheck(RejectAttributeChecker.DEFINED, EJB3SubsystemRootResourceDefinition.DISABLE_DEFAULT_EJB_PERMISSIONS);
        // We can always discard this attribute, because it's meaningless without the security-manager subsystem, and
        // a legacy slave can't have that subsystem in its profile.
        builder.getAttributeBuilder().setDiscard(new DiscardAttributeChecker.DiscardAttributeValueChecker(new ModelNode(false)), EJB3SubsystemRootResourceDefinition.DISABLE_DEFAULT_EJB_PERMISSIONS);
        //builder.getAttributeBuilder().setValueConverter(AttributeConverter.Factory.createHardCoded(new ModelNode("hornetq-ra"), true), EJB3SubsystemRootResourceDefinition.DEFAULT_RESOURCE_ADAPTER_NAME);

        builder.getAttributeBuilder().setDiscard(new DiscardAttributeChecker.DiscardAttributeValueChecker(new ModelNode(false)), EJB3SubsystemRootResourceDefinition.ALLOW_EJB_NAME_REGEX);
        builder.getAttributeBuilder().addRejectCheck(RejectAttributeChecker.DEFINED, EJB3SubsystemRootResourceDefinition.ALLOW_EJB_NAME_REGEX);

        builder.getAttributeBuilder().setDiscard(new DiscardAttributeChecker.DiscardAttributeValueChecker(new ModelNode(false)), EJB3SubsystemRootResourceDefinition.ENABLE_GRACEFUL_TXN_SHUTDOWN)
                .addRejectCheck(RejectAttributeChecker.DEFINED, EJB3SubsystemRootResourceDefinition.ENABLE_GRACEFUL_TXN_SHUTDOWN);

        registerPassivationStoreTransformers_1_2_1_and_1_3_0(builder);
        registerRemoteTransformers(builder);
        registerMdbDeliveryGroupTransformers(builder);
        registerStrictMaxPoolTransformers(builder);
        registerApplicationSecurityDomainDTransformers(builder);
        registerIdentityTransformers(builder);
        builder.rejectChildResource(PathElement.pathElement(EJB3SubsystemModel.REMOTING_PROFILE));
        if (version.equals(VERSION_1_2_1)) {
            registerTimerTransformers_1_2_0(builder);
        } else if (version.equals(VERSION_1_3_0)) {
            registerTimerTransformers_1_3_0(builder);
        }

        // Rename new statistics-enabled attribute to old enable-statistics
        builder.getAttributeBuilder()
               .addRename(EJB3SubsystemModel.STATISTICS_ENABLED, EJB3SubsystemModel.ENABLE_STATISTICS);

        TransformationDescription.Tools.register(builder.build(), subsystemRegistration, version);
    }

    private static void registerTransformers_3_0_0(SubsystemTransformerRegistration subsystemRegistration) {
        final ResourceTransformationDescriptionBuilder builder = TransformationDescriptionBuilder.Factory.createSubsystemInstance();
        builder.getAttributeBuilder().setValueConverter(AttributeConverter.Factory.createHardCoded(new ModelNode("hornetq-ra"), true), EJB3SubsystemRootResourceDefinition.DEFAULT_RESOURCE_ADAPTER_NAME)
                .end();
        builder.getAttributeBuilder().setDiscard(new DiscardAttributeChecker.DiscardAttributeValueChecker(new ModelNode(false)), EJB3SubsystemRootResourceDefinition.ALLOW_EJB_NAME_REGEX);
        builder.getAttributeBuilder().addRejectCheck(RejectAttributeChecker.DEFINED, EJB3SubsystemRootResourceDefinition.ALLOW_EJB_NAME_REGEX);
        builder.getAttributeBuilder().setDiscard(new DiscardAttributeChecker.DiscardAttributeValueChecker(new ModelNode(false)), EJB3SubsystemRootResourceDefinition.ENABLE_GRACEFUL_TXN_SHUTDOWN);
        builder.getAttributeBuilder().addRejectCheck(RejectAttributeChecker.DEFINED, EJB3SubsystemRootResourceDefinition.ENABLE_GRACEFUL_TXN_SHUTDOWN);
        registerMdbDeliveryGroupTransformers(builder);
        registerRemoteTransformers(builder);
        registerStrictMaxPoolTransformers(builder);
        registerApplicationSecurityDomainDTransformers(builder);
        registerIdentityTransformers(builder);

        // Rename new statistics-enabled attribute to old enable-statistics
        builder.getAttributeBuilder().addRename(EJB3SubsystemModel.STATISTICS_ENABLED, EJB3SubsystemModel.ENABLE_STATISTICS);
        TransformationDescription.Tools.register(builder.build(), subsystemRegistration, VERSION_3_0_0);
    }

    private static void registerTransformers_4_0_0(SubsystemTransformerRegistration subsystemRegistration) {
        final ResourceTransformationDescriptionBuilder builder = TransformationDescriptionBuilder.Factory.createSubsystemInstance();

        registerApplicationSecurityDomainDTransformers(builder);
        registerIdentityTransformers(builder);
        builder.addChildResource(RemotingProfileResourceDefinition.INSTANCE).getAttributeBuilder()
                .addRejectCheck(RejectAttributeChecker.DEFINED, StaticEJBDiscoveryDefinition.INSTANCE)
                .end();

        builder.getAttributeBuilder().setDiscard(new DiscardAttributeChecker.DiscardAttributeValueChecker(new ModelNode(false)), EJB3SubsystemRootResourceDefinition.ALLOW_EJB_NAME_REGEX);
        builder.getAttributeBuilder().addRejectCheck(RejectAttributeChecker.DEFINED, EJB3SubsystemRootResourceDefinition.ALLOW_EJB_NAME_REGEX);

        builder.getAttributeBuilder().setDiscard(new DiscardAttributeChecker.DiscardAttributeValueChecker(new ModelNode(false)), EJB3SubsystemRootResourceDefinition.ENABLE_GRACEFUL_TXN_SHUTDOWN);
        builder.getAttributeBuilder().addRejectCheck(RejectAttributeChecker.DEFINED, EJB3SubsystemRootResourceDefinition.ENABLE_GRACEFUL_TXN_SHUTDOWN);

        // Rename new statistics-enabled attribute to old enable-statistics
        builder.getAttributeBuilder()
               .addRename(EJB3SubsystemModel.STATISTICS_ENABLED, EJB3SubsystemModel.ENABLE_STATISTICS);

        TransformationDescription.Tools.register(builder.build(), subsystemRegistration, VERSION_4_0_0);
    }

    private static void registerRemoteTransformers(ResourceTransformationDescriptionBuilder parent) {
        ResourceTransformationDescriptionBuilder remoteService = parent.addChildResource(EJB3SubsystemModel.REMOTE_SERVICE_PATH);
        remoteService.getAttributeBuilder()
                .setDiscard(new DiscardAttributeChecker.DiscardAttributeValueChecker(new ModelNode(BeanManagerFactoryServiceConfiguratorConfiguration.DEFAULT_CONTAINER_NAME)), CLIENT_MAPPINGS_CLUSTER_NAME)
                .addRejectCheck(RejectAttributeChecker.DEFINED, CLIENT_MAPPINGS_CLUSTER_NAME)
                .setDiscard(DiscardAttributeChecker.ALWAYS, EXECUTE_IN_WORKER) //as this does not affect functionality we just discard
                .end();
    }

    private  static void registerIdentityTransformers(ResourceTransformationDescriptionBuilder parent) {
        parent.rejectChildResource(EJB3SubsystemModel.IDENTITY_PATH);
    }

    private  static void registerApplicationSecurityDomainDTransformers(ResourceTransformationDescriptionBuilder parent) {
        parent.rejectChildResource(PathElement.pathElement(EJB3SubsystemModel.APPLICATION_SECURITY_DOMAIN));
    }

    private static void registerStrictMaxPoolTransformers(ResourceTransformationDescriptionBuilder parent) {
        parent.addChildResource(PathElement.pathElement(EJB3SubsystemModel.STRICT_MAX_BEAN_INSTANCE_POOL))
                .getAttributeBuilder()
                .setDiscard(new DiscardAttributeChecker.DiscardAttributeValueChecker(new ModelNode(StrictMaxPoolResourceDefinition.DeriveSize.NONE.toString())), DERIVE_SIZE)
                .addRejectCheck(RejectAttributeChecker.DEFINED, DERIVE_SIZE);
    }

    private static void registerMdbDeliveryGroupTransformers(ResourceTransformationDescriptionBuilder parent) {
        parent.rejectChildResource(PathElement.pathElement(EJB3SubsystemModel.MDB_DELIVERY_GROUP));
    }

    private static void registerTimerTransformers_1_2_0(ResourceTransformationDescriptionBuilder parent) {
        ResourceTransformationDescriptionBuilder timerService = parent.addChildResource(EJB3SubsystemModel.TIMER_SERVICE_PATH);
        registerDataStoreTransformers(timerService);
    }

    private static void registerDataStoreTransformers(ResourceTransformationDescriptionBuilder timerService) {

        DataStoreTransformer dataStoreTransformer = new DataStoreTransformer();
        timerService.getAttributeBuilder()
                .setDiscard(DiscardAttributeChecker.ALWAYS, EJB3SubsystemModel.DEFAULT_DATA_STORE)//this is ok, as default-data-store only has any sense with new model, but it is always set!
                .end();
        timerService.discardOperations(ModelDescriptionConstants.ADD);
        timerService.setCustomResourceTransformer(dataStoreTransformer);
        timerService.rejectChildResource(EJB3SubsystemModel.DATABASE_DATA_STORE_PATH);
        ResourceTransformationDescriptionBuilder fileDataStore = timerService.addChildRedirection(EJB3SubsystemModel.FILE_DATA_STORE_PATH, (current, builder) -> builder.getCurrent());

        fileDataStore.addOperationTransformationOverride(ModelDescriptionConstants.ADD)
                .inheritResourceAttributeDefinitions()
                .setCustomOperationTransformer(dataStoreTransformer)
                .end();

    }

    private static void registerTimerTransformers_1_3_0(ResourceTransformationDescriptionBuilder parent) {
        ResourceTransformationDescriptionBuilder timerService = parent.addChildResource(EJB3SubsystemModel.TIMER_SERVICE_PATH);
        ResourceTransformationDescriptionBuilder db = timerService.addChildResource(EJB3SubsystemModel.DATABASE_DATA_STORE_PATH);
                db.getAttributeBuilder()
                        .setDiscard(new DiscardAttributeChecker.DiscardAttributeValueChecker(new ModelNode(-1)), REFRESH_INTERVAL)
                        .setDiscard(new DiscardAttributeChecker.DiscardAttributeValueChecker(new ModelNode(true)), ALLOW_EXECUTION)
                        .addRejectCheck(RejectAttributeChecker.DEFINED, REFRESH_INTERVAL, ALLOW_EXECUTION);
    }

    private static class DataStoreTransformer implements CombinedTransformer {

        private DataStoreTransformer() {
        }

        @Override
        public TransformedOperation transformOperation(final TransformationContext context, final PathAddress address, final ModelNode operation) throws OperationFailedException {
            Resource original = context.readResourceFromRoot(address);
            String defaultDataStore = original.getModel().get(TimerServiceResourceDefinition.DEFAULT_DATA_STORE.getName()).asString();
            boolean hasFileDataStore = original.hasChild(PathElement.pathElement(EJB3SubsystemModel.FILE_DATA_STORE_PATH.getKey(), defaultDataStore));
            if (original.getChildren(EJB3SubsystemModel.FILE_DATA_STORE).size() > 1 ||
                    !hasFileDataStore) {
                return new TransformedOperation(operation, new OperationRejectionPolicy() {
                    @Override
                    public boolean rejectOperation(ModelNode preparedResult) {
                        return true;
                    }

                    @Override
                    public String getFailureDescription() {
                        return context.getLogger().getRejectedResourceWarning(address, operation);
                    }
                }, OperationResultTransformer.ORIGINAL_RESULT);
            }
            operation.get(TimerServiceResourceDefinition.THREAD_POOL_NAME.getName()).set(original.getModel().get(TimerServiceResourceDefinition.THREAD_POOL_NAME.getName()));
            return new TransformedOperation(operation, OperationResultTransformer.ORIGINAL_RESULT);
        }

        @Override
        public void transformResource(ResourceTransformationContext context, PathAddress address, Resource resource) throws OperationFailedException {
            Resource untransformedResource = context.readResource(PathAddress.EMPTY_ADDRESS);
            ModelNode untransformedModel = Resource.Tools.readModel(untransformedResource);
            String defaultDataStore = untransformedModel.get(TimerServiceResourceDefinition.DEFAULT_DATA_STORE.getName()).asString();

            ModelNode transformed = resource.getModel();
            transformed.remove(TimerServiceResourceDefinition.DEFAULT_DATA_STORE.getName());
            ModelNode fileStore = untransformedModel.get(EJB3SubsystemModel.FILE_DATA_STORE, defaultDataStore);
            if (!fileStore.isDefined()) {//happens where default is not file-store
                rejectIncompatibleDataStores(context, address);
            } else if ((untransformedModel.hasDefined(EJB3SubsystemModel.DATABASE_DATA_STORE)
                    && untransformedModel.get(EJB3SubsystemModel.DATABASE_DATA_STORE).keys().size() > 0)
                    || untransformedModel.get(EJB3SubsystemModel.FILE_DATA_STORE).keys().size() > 1) {
                rejectIncompatibleDataStores(context, address);
            }

            ModelNode path = fileStore.get(EJB3SubsystemModel.PATH);
            transformed.get(EJB3SubsystemModel.PATH).set(path);
            transformed.get(EJB3SubsystemModel.RELATIVE_TO).set(fileStore.get(EJB3SubsystemModel.RELATIVE_TO));

            context.addTransformedResource(PathAddress.EMPTY_ADDRESS, resource);
            //do not process children!
        }

        private void rejectIncompatibleDataStores(ResourceTransformationContext context, PathAddress address) throws OperationFailedException {
            throw new OperationFailedException(EjbLogger.ROOT_LOGGER.untransformableTimerService(address));
        }
    }

    /*
        * This transformer does the following:
        * - maps <passivation-store/> to <cluster-passivation-store/>
        * - sets appropriate defaults for IDLE_TIMEOUT, IDLE_TIMEOUT_UNIT, PASSIVATE_EVENTS_ON_REPLICATE, and CLIENT_MAPPINGS_CACHE
        */
    @SuppressWarnings("deprecation")
    private static void registerPassivationStoreTransformers_1_2_1_and_1_3_0(ResourceTransformationDescriptionBuilder parent) {

        ResourceTransformationDescriptionBuilder child = parent.addChildRedirection(PassivationStoreResourceDefinition.INSTANCE.getPathElement(), PathElement.pathElement(EJB3SubsystemModel.CLUSTER_PASSIVATION_STORE));
        child.getAttributeBuilder()
                .setValueConverter(AttributeConverter.Factory.createHardCoded(new ModelNode(true), true), EJB3SubsystemModel.PASSIVATE_EVENTS_ON_REPLICATE)
                .setValueConverter(AttributeConverter.Factory.createHardCoded(new ModelNode("default"), true), EJB3SubsystemModel.CLIENT_MAPPINGS_CACHE)
                .setValueConverter(AttributeConverter.Factory.createHardCoded(new ModelNode().set(Long.valueOf(Integer.MAX_VALUE)), true), EJB3SubsystemModel.IDLE_TIMEOUT)
                .setValueConverter(AttributeConverter.Factory.createHardCoded(new ModelNode().set(TimeUnit.SECONDS.name()), true), EJB3SubsystemModel.IDLE_TIMEOUT_UNIT)
        ;
    }

    /**
     * This Combined Transformer manages this transformation from EAP7 to previous versions (attribute definition/xml markup):
     *
     * DEFAULT_SFSB_CACHE / cache-ref --> DEFAULT_CLUSTERED_SFSB_CACHE / clustered-cache-ref
     * DEFAULT_SFSB_PASSIVATION_DISABLED_CACHE / passivation-disabled-cache-ref --> DEFAULT_SFSB_CACHE / cache-ref
     *
     */
    private static class StatefulCacheRefTransformer implements CombinedTransformer {
        private final Map<String, String> renames;

        public StatefulCacheRefTransformer(){
            renames = new HashMap<>();
            renames.put(DEFAULT_SFSB_CACHE, EJB3SubsystemModel.DEFAULT_CLUSTERED_SFSB_CACHE);
            renames.put(DEFAULT_SFSB_PASSIVATION_DISABLED_CACHE, EJB3SubsystemModel.DEFAULT_SFSB_CACHE);
        }

        @Override
        public TransformedOperation transformOperation(TransformationContext context, PathAddress address, ModelNode operation) {
            if (operation != null && !(operation.hasDefined(OPERATION_HEADERS) && operation.get(OPERATION_HEADERS, "push-to-servers").asBoolean(false)) ){
                String originalAttribute = Operations.getAttributeName(operation);
                if (renames.containsKey(originalAttribute)){
                    operation = operation.clone();
                    operation.get(NAME).set(renames.get(originalAttribute));
                }
            }

            return new TransformedOperation(operation, OperationResultTransformer.ORIGINAL_RESULT);
        }

        @Override
        public void transformResource(ResourceTransformationContext context, PathAddress address, Resource resource) throws OperationFailedException {
            Resource untransformedResource = context.readResource(PathAddress.EMPTY_ADDRESS);
            ModelNode untransformedModel = Resource.Tools.readModel(untransformedResource);

            String statefulCache = untransformedModel.get(DEFAULT_SFSB_CACHE).asString();
            String statefulPassivationDisabledCache = untransformedModel.get(DEFAULT_SFSB_PASSIVATION_DISABLED_CACHE).asString();

            ModelNode transformed = resource.getModel();
            transformed.get(DEFAULT_SFSB_CACHE).set(statefulPassivationDisabledCache);
            transformed.get(EJB3SubsystemModel.DEFAULT_CLUSTERED_SFSB_CACHE).set(statefulCache);
            transformed.remove(DEFAULT_SFSB_PASSIVATION_DISABLED_CACHE);

            final ResourceTransformationContext childContext = context.addTransformedResource(PathAddress.EMPTY_ADDRESS, resource);
            childContext.processChildren(resource);
        }
    }

    private static class AddStatefulCacheRefTransformer implements OperationTransformer{

        @Override
        public TransformedOperation transformOperation(TransformationContext context, PathAddress address, ModelNode operation) {
            if (operation != null && !(operation.hasDefined(OPERATION_HEADERS) && operation.get(OPERATION_HEADERS, "push-to-servers").asBoolean(false)) ){
                operation = operation.clone();

                String statefulCache = operation.get(DEFAULT_SFSB_CACHE).asString();
                String statefulPassivationDisabledCache = operation.get(DEFAULT_SFSB_PASSIVATION_DISABLED_CACHE).asString();

                operation.get(DEFAULT_SFSB_CACHE).set(statefulPassivationDisabledCache);
                operation.get(EJB3SubsystemModel.DEFAULT_CLUSTERED_SFSB_CACHE).set(statefulCache);
                operation.remove(DEFAULT_SFSB_PASSIVATION_DISABLED_CACHE);
            }
            return new TransformedOperation(operation, OperationResultTransformer.ORIGINAL_RESULT);
        }
    }

}


/*
 * JBoss, Home of Professional Open Source.
 * Copyright 2014, Red Hat, Inc., and individual contributors
 * as indicated by the @author tags. See the copyright.txt file in the
 * distribution for a full listing of individual contributors.
 *
 * This is free software; you can redistribute it and/or modify it
 * under the terms of the GNU Lesser General Public License as
 * published by the Free Software Foundation; either version 2.1 of
 * the License, or (at your option) any later version.
 *
 * This software is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this software; if not, write to the Free
 * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
 * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
 */

package org.wildfly.iiop.openjdk.service;

import java.util.ArrayList;
import java.util.List;

import com.sun.corba.se.spi.extension.ZeroPortPolicy;
import org.jboss.msc.inject.Injector;
import org.jboss.msc.service.Service;
import org.jboss.msc.service.ServiceName;
import org.jboss.msc.service.StartContext;
import org.jboss.msc.service.StartException;
import org.jboss.msc.service.StopContext;
import org.jboss.msc.value.InjectedValue;
import org.omg.CORBA.ORB;
import org.omg.CORBA.Policy;
import org.omg.PortableServer.IdAssignmentPolicyValue;
import org.omg.PortableServer.IdUniquenessPolicyValue;
import org.omg.PortableServer.ImplicitActivationPolicyValue;
import org.omg.PortableServer.LifespanPolicyValue;
import org.omg.PortableServer.POA;
import org.omg.PortableServer.POAHelper;
import org.omg.PortableServer.RequestProcessingPolicyValue;
import org.omg.PortableServer.ServantRetentionPolicyValue;
import org.omg.PortableServer.ThreadPolicyValue;
import org.wildfly.iiop.openjdk.IIOPExtension;
import org.wildfly.iiop.openjdk.logging.IIOPLogger;

/**
 *

 * This class implements a service that creates and activates {@code org.omg.PortableServer.POA} objects.
 *

 *
 * @author Stefan Guilhen
 * @author Tomasz Adamski
 */
public class CorbaPOAService implements Service<POA> {

    public static final ServiceName SERVICE_NAME = ServiceName.JBOSS.append(IIOPExtension.SUBSYSTEM_NAME, "poa-service");

    public static final ServiceName ROOT_SERVICE_NAME = SERVICE_NAME.append("rootpoa");

    public static final ServiceName INTERFACE_REPOSITORY_SERVICE_NAME = SERVICE_NAME.append("irpoa");

    private volatile POA poa;

    private final InjectedValue<ORB> orbInjector = new InjectedValue<ORB>();

    private final InjectedValue<POA> parentPOAInjector = new InjectedValue<POA>();

    private final String poaName;

    private final String bindingName;

    // the policy values that can be assigned to created POAs.

    private final IdAssignmentPolicyValue idAssignmentPolicyValue;

    private final IdUniquenessPolicyValue idUniquenessPolicyValue;

    private final ImplicitActivationPolicyValue implicitActivationPolicyValue;

    private final LifespanPolicyValue lifespanPolicyValue;

    private final RequestProcessingPolicyValue requestProcessingPolicyValue;

    private final ServantRetentionPolicyValue servantRetentionPolicyValue;

    private final ThreadPolicyValue threadPolicyValue;

    private final boolean sslOnly;

    /**
     *

     * Creates a {@code CorbaPOAService} with the specified POA name and binding name. The {@code POA} created by this
     * service will not be associated with any policies.
     *

     *
     * @param poaName     the name of the {@code POA} that will be created by this service (ex. "RootPOA").
     * @param bindingName the JNDI context name where the created {@code POA} will be bound. If null, the JNDI binding
     *                    won't be performed.
     */
    public CorbaPOAService(String poaName, String bindingName, boolean sslOnly) {
        this(poaName, bindingName, sslOnly, null, null, null, null, null, null, null);
    }

    /**
     *

     * Creates a {@code CorbaPOAService} with the specified POA name, binding name and policy values.
     *

     *
     * @param poaName                       the name of the {@code POA} that will be created by this service (ex. "RootPOA").
     * @param bindingName                   the JNDI context name where the created {@code POA} will be bound. If null, the JNDI binding
     *                                      won't be performed.
     * @param idAssignmentPolicyValue       the {@code IdAssignmentPolicyValue} that will be associated with the created
     *                                      {@code POA}. Can be null.
     * @param idUniquenessPolicyValue       the {@code IdUniquenessPolicyValue} that will be associated with the created
     *                                      {@code POA}. Can be null.
     * @param implicitActivationPolicyValue the {@code ImplicitActivationPolicyValue} that will be associated with the
     *                                      created {@code POA}. Can be null.
     * @param lifespanPolicyValue           the {@code LifespanPolicyValue} that will be associated with the created {@code POA}.
     *                                      Can be null.
     * @param requestProcessingPolicyValue  the {@code RequestProcessingPolicyValue} that will be associated with the
     *                                      created {@code POA}. Can be null.
     * @param servantRetentionPolicyValue   the {@code ServantRetentionPolicyValue} that will be associated with the created
     *                                      {@code POA}. Can be null.
     * @param threadPolicyValue             the {@code ThreadPolicyValue} that will be associated with the created {@code POA}. Can
     *                                      be null.
     */
    public CorbaPOAService(String poaName, String bindingName, boolean sslOnly, IdAssignmentPolicyValue idAssignmentPolicyValue,
                           IdUniquenessPolicyValue idUniquenessPolicyValue, ImplicitActivationPolicyValue implicitActivationPolicyValue,
                           LifespanPolicyValue lifespanPolicyValue, RequestProcessingPolicyValue requestProcessingPolicyValue,
                           ServantRetentionPolicyValue servantRetentionPolicyValue, ThreadPolicyValue threadPolicyValue) {
        this.poaName = poaName;
        this.bindingName = bindingName;
        this.sslOnly = sslOnly;
        this.idAssignmentPolicyValue = idAssignmentPolicyValue;
        this.idUniquenessPolicyValue = idUniquenessPolicyValue;
        this.implicitActivationPolicyValue = implicitActivationPolicyValue;
        this.lifespanPolicyValue = lifespanPolicyValue;
        this.requestProcessingPolicyValue = requestProcessingPolicyValue;
        this.servantRetentionPolicyValue = servantRetentionPolicyValue;
        this.threadPolicyValue = threadPolicyValue;
    }

    @Override
    public void start(StartContext context) throws StartException {
        if (IIOPLogger.ROOT_LOGGER.isDebugEnabled()) {
            IIOPLogger.ROOT_LOGGER.debugf("Starting service %s", context.getController().getName().getCanonicalName());
        }

        ORB orb = this.orbInjector.getOptionalValue();
        POA parentPOA = this.parentPOAInjector.getOptionalValue();

        // if an ORB has been injected, we will use the ORB.resolve_initial_references method to instantiate the POA.
        if (orb != null) {
            try {
                this.poa = POAHelper.narrow(orb.resolve_initial_references(this.poaName));
            } catch (Exception e) {
                throw IIOPLogger.ROOT_LOGGER.errorResolvingInitRef(this.poaName, e);
            }
        }
        // if a parent POA has been injected, we use it to create the policies and then the POA itself.
        else if (parentPOA != null) {
            try {
                Policy[] poaPolicies = this.createPolicies(parentPOA);
                this.poa = parentPOA.create_POA(this.poaName, null, poaPolicies);
            } catch (Exception e) {
                throw IIOPLogger.ROOT_LOGGER.errorCreatingPOAFromParent(e);
            }
        } else {
            throw IIOPLogger.ROOT_LOGGER.invalidPOACreationArgs();
        }

        // check if the POA should be bound to JNDI under java:/jboss.
        if (this.bindingName != null) {
            CorbaServiceUtil.bindObject(context.getChildTarget(), this.bindingName, this.poa);
        }

        // activate the created POA.
        try {
            this.poa.the_POAManager().activate();
        } catch (Exception e) {
            throw IIOPLogger.ROOT_LOGGER.errorActivatingPOA(e);
        }
    }

    @Override
    public void stop(StopContext context) {
        if (IIOPLogger.ROOT_LOGGER.isDebugEnabled()) {
            IIOPLogger.ROOT_LOGGER.debugf("Stopping service %s", context.getController().getName().getCanonicalName());
        }

        // destroy parent POAs, letting they destroy their children POAs in the process.
        if (this.poa.the_parent() == null)
           this.poa.destroy(false, true);
    }

    @Override
    public POA getValue() throws IllegalStateException, IllegalArgumentException {
        return this.poa;
    }

    public Injector<ORB> getORBInjector() {
        return this.orbInjector;
    }

    public Injector<POA> getParentPOAInjector() {
        return this.parentPOAInjector;
    }

    /**
     *

     * Create the {@code Policy} array containing the {@code POA} policies using the values specified in the constructor.
     * When creating a {@code POA}, the parent {@code POA} is responsible for generating the relevant policies beforehand.
     *

     *
     * @param poa the {@code POA} used to create the {@code Policy} objects.
     * @return the constructed {@code Policy} array.
     */
    private Policy[] createPolicies(POA poa) {
        List<Policy> policies = new ArrayList<Policy>();
        if(this.sslOnly)
            policies.add(ZeroPortPolicy.getPolicy());
        if (this.idAssignmentPolicyValue != null)
            policies.add(poa.create_id_assignment_policy(this.idAssignmentPolicyValue));
        if (this.idUniquenessPolicyValue != null)
            policies.add(poa.create_id_uniqueness_policy(this.idUniquenessPolicyValue));
        if (this.implicitActivationPolicyValue != null)
            policies.add(poa.create_implicit_activation_policy(this.implicitActivationPolicyValue));
        if (this.lifespanPolicyValue != null)
            policies.add(poa.create_lifespan_policy(this.lifespanPolicyValue));
        if (this.requestProcessingPolicyValue != null)
            policies.add(poa.create_request_processing_policy(this.requestProcessingPolicyValue));
        if (this.servantRetentionPolicyValue != null)
            policies.add(poa.create_servant_retention_policy(this.servantRetentionPolicyValue));
        if (this.threadPolicyValue != null)
            policies.add(poa.create_thread_policy(this.threadPolicyValue));

        return policies.toArray(new Policy[policies.size()]);
    }
}


/*
 * JBoss, Home of Professional Open Source.
 * Copyright 2008, Red Hat Middleware LLC, and individual contributors
 * as indicated by the @author tags. See the copyright.txt file in the
 * distribution for a full listing of individual contributors.
 *
 * This is free software; you can redistribute it and/or modify it
 * under the terms of the GNU Lesser General Public License as
 * published by the Free Software Foundation; either version 2.1 of
 * the License, or (at your option) any later version.
 *
 * This software is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this software; if not, write to the Free
 * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
 * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
 */
package org.jboss.as.ejb3.iiop.stub;
// because it calls some ProxyAssembler
// methods that currently are package
// accessible

import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.rmi.RemoteException;

import org.jboss.as.server.deployment.ModuleClassFactory;
import org.jboss.as.ejb3.logging.EjbLogger;
import org.jboss.classfilewriter.ClassFile;
import org.jboss.classfilewriter.ClassMethod;
import org.jboss.classfilewriter.code.CodeAttribute;
import org.jboss.classfilewriter.util.Boxing;
import org.jboss.classfilewriter.util.DescriptorUtils;
import org.wildfly.iiop.openjdk.rmi.AttributeAnalysis;
import org.wildfly.iiop.openjdk.rmi.ExceptionAnalysis;
import org.wildfly.iiop.openjdk.rmi.InterfaceAnalysis;
import org.wildfly.iiop.openjdk.rmi.OperationAnalysis;
import org.wildfly.iiop.openjdk.rmi.RMIIIOPViolationException;
import org.wildfly.iiop.openjdk.rmi.marshal.CDRStream;
import org.wildfly.iiop.openjdk.rmi.marshal.strategy.StubStrategy;

/**
 * Utility class responsible for the dynamic generation of bytecodes of
 * IIOP stub classes.
 *
 * @author Stuart Douglas
 * @author Francisco Reverbel
 * @author Richard Opalka
 */
public class IIOPStubCompiler {

    public static final String ID_FIELD_NAME = "$ids";

    /**
     * Returns the name of the stub strategy field associated with the method
     * whose index is methodIndex.
     */
    private static String strategy(int methodIndex) {
        return "$s" + methodIndex;
    }

    /**
     * Returns the name of static initializer method associated with the method
     * whose index is methodIndex.
     */
    private static String init(int methodIndex) {
        return "$i" + methodIndex;
    }

    /**
     * Generates the code of a given method within a stub class.
     *
     * @param asm           the ProxyAssembler used to assemble
     *                      the method code
     * @param superclass    the superclass of the stub class within which the
     *                      method will be generated
     * @param m             a Method instance describing the
     *                      method declaration by an RMI/IDL interface
     * @param idlName       a string with the method name mapped to IDL
     * @param strategyField a string with the name of the strategy field that
     *                      will be associated with the generated method
     * @param initMethod    a string with the name of the static initialization
     *                      method that will be associated with the generated
     *                      method.
     */
    private static void generateMethodCode(ClassFile asm,
                                           Class<?> superclass,
                                           Method m,
                                           String idlName,
                                           String strategyField,
                                           String initMethod) {
        Class<?> returnType = m.getReturnType();
        Class<?>[] paramTypes = m.getParameterTypes();
        Class<?>[] exceptions = m.getExceptionTypes();

        // Generate a static field with the StubStrategy for the method
        asm.addField(Modifier.PRIVATE + Modifier.STATIC, strategyField, StubStrategy.class);

        // Generate the method code
        final CodeAttribute ca = asm.addMethod(m).getCodeAttribute();

        // The method code issues a call
        // super.invoke*(idlName, strategyField, args)
        ca.aload(0);
        ca.ldc(idlName);
        ca.getstatic(asm.getName(), strategyField, StubStrategy.class);

        // Push args
        if (paramTypes.length == 0) {
            ca.iconst(0);
            ca.anewarray(Object.class.getName());
            //asm.pushField(Util.class, "NOARGS");
        } else {
            ca.iconst(paramTypes.length);
            ca.anewarray(Object.class.getName());
            int index = 1;
            for (int j = 0; j < paramTypes.length; j++) {

                Class<?> type = paramTypes[j];

                ca.dup();
                ca.iconst(j);
                if (!type.isPrimitive()) {
                    // object or array
                    ca.aload(index);
                } else if (type.equals(double.class)) {
                    ca.dload(index);
                    Boxing.boxDouble(ca);
                    index++;
                } else if (type.equals(long.class)) {
                    ca.lload(index);
                    Boxing.boxLong(ca);
                    index++;
                } else if (type.equals(float.class)) {
                    ca.fload(index);
                    Boxing.boxFloat(ca);
                } else {
                    ca.iload(index);
                    Boxing.boxIfNessesary(ca, DescriptorUtils.makeDescriptor(type));
                }
                index++;
                ca.aastore();
            }
        }
        // Generate the call to an invoke* method ot the superclass
        String invoke = "invoke";
        String ret = "Ljava/lang/Object;";
        if (returnType.isPrimitive() && returnType != Void.TYPE) {
            String typeName = returnType.getName();
            invoke += (Character.toUpperCase(typeName.charAt(0))
                    + typeName.substring(1));
            ret = DescriptorUtils.makeDescriptor(returnType);
        }
        ca.invokevirtual(superclass.getName(), invoke, "(Ljava/lang/String;Lorg/wildfly/iiop/openjdk/rmi/marshal/strategy/StubStrategy;[Ljava/lang/Object;)" + ret);
        if (!returnType.isPrimitive() && returnType != Object.class) {
            ca.checkcast(returnType);
        }
        ca.returnInstruction();

        // Generate a static method that initializes the method's strategy field
        final CodeAttribute init = asm.addMethod(Modifier.PRIVATE + Modifier.STATIC,initMethod, "V").getCodeAttribute();
            int i;
            int len;

            // Push first argument for StubStrategy constructor:
            // array with abbreviated names of the param marshallers
            len = paramTypes.length;
            init.iconst(len);
            init.anewarray(String.class.getName());
            for (i = 0; i < len; i++) {
                init.dup();
                init.iconst(i);
                init.ldc(CDRStream.abbrevFor(paramTypes[i]));
                init.aastore();
            }

            // Push second argument for StubStrategy constructor:
            // array with exception repository ids
            len = exceptions.length;
            int n = 0;
            for (i = 0; i < len; i++) {
                if (!RemoteException.class.isAssignableFrom(exceptions[i])) {
                    n++;
                }
            }
            init.iconst(n);
            init.anewarray(String.class.getName());
            try {
                int j = 0;
                for (i = 0; i < len; i++) {
                    if (!RemoteException.class.isAssignableFrom(exceptions[i])) {
                        init.dup();
                        init.iconst(j);
                        init.ldc(
                                ExceptionAnalysis.getExceptionAnalysis(exceptions[i])
                                        .getExceptionRepositoryId());
                        init.aastore();
                        j++;
                    }
                }
            } catch (RMIIIOPViolationException e) {
                throw EjbLogger.ROOT_LOGGER.exceptionRepositoryNotFound(exceptions[i].getName(), e.getLocalizedMessage());
            }

            // Push third argument for StubStrategy constructor:
            // array with exception class names
            init.iconst(n);
            init.anewarray(String.class.getName());
            int j = 0;
            for (i = 0; i < len; i++) {
                if (!RemoteException.class.isAssignableFrom(exceptions[i])) {
                    init.dup();
                    init.iconst(j);
                    init.ldc(exceptions[i].getName());
                    init.aastore();
                    j++;
                }
            }

            // Push fourth argument for StubStrategy constructor:
            // abbreviated name of the return value marshaller
            init.ldc(CDRStream.abbrevFor(returnType));

            // Push fifth argument for StubStrategy constructor:
            // null (no ClassLoader specified)
            init.aconstNull();

            // Constructs the StubStrategy
            init.invokestatic(StubStrategy.class.getName(), "forMethod", "([Ljava/lang/String;[Ljava/lang/String;[Ljava/lang/String;Ljava/lang/String;Ljava/lang/ClassLoader;)Lorg/wildfly/iiop/openjdk/rmi/marshal/strategy/StubStrategy;");

            // Set the strategy field of this stub class
            init.putstatic(asm.getName(), strategyField, StubStrategy.class);

            init.returnInstruction();
    }

    /**
     * Generates the bytecodes of a stub class for a given interface.
     *
     * @param interfaceAnalysis an InterfaceAnalysis instance
     *                        describing the RMI/IIOP interface to be
     *                        implemented by the stub class
     * @param superclass      the superclass of the stub class
     * @param stubClassName   the name of the stub class
     * @return a byte array with the generated bytecodes.
     */
    private static ClassFile generateCode(InterfaceAnalysis interfaceAnalysis,
                                       Class<?> superclass, String stubClassName) {
        final ClassFile asm =
                new ClassFile(stubClassName, superclass.getName(), null, ModuleClassFactory.INSTANCE, interfaceAnalysis.getCls().getName());

        int methodIndex = 0;

        AttributeAnalysis[] attrs = interfaceAnalysis.getAttributes();
        for (int i = 0; i < attrs.length; i++) {
            OperationAnalysis op = attrs[i].getAccessorAnalysis();
            generateMethodCode(asm, superclass, op.getMethod(), op.getIDLName(),
                    strategy(methodIndex), init(methodIndex));
            methodIndex++;
            op = attrs[i].getMutatorAnalysis();
            if (op != null) {
                generateMethodCode(asm, superclass,
                        op.getMethod(), op.getIDLName(),
                        strategy(methodIndex), init(methodIndex));
                methodIndex++;
            }
        }

        final OperationAnalysis[] ops = interfaceAnalysis.getOperations();
        for (int i = 0; i < ops.length; i++) {
            generateMethodCode(asm, superclass,
                    ops[i].getMethod(), ops[i].getIDLName(),
                    strategy(methodIndex), init(methodIndex));
            methodIndex++;
        }

        // Generate the constructor
        final ClassMethod ctor = asm.addMethod(Modifier.PUBLIC, "<init>", "V");
        ctor.getCodeAttribute().aload(0);
        ctor.getCodeAttribute().invokespecial(superclass.getName(), "<init>", "()V");
        ctor.getCodeAttribute().returnInstruction();

        // Generate the method _ids(), declared as abstract in ObjectImpl
        final String[] ids = interfaceAnalysis.getAllTypeIds();
        asm.addField(Modifier.PRIVATE + Modifier.STATIC, ID_FIELD_NAME, String[].class);
        final CodeAttribute idMethod = asm.addMethod(Modifier.PUBLIC + Modifier.FINAL, "_ids", "[Ljava/lang/String;").getCodeAttribute();
        idMethod.getstatic(stubClassName, ID_FIELD_NAME, "[Ljava/lang/String;");
        idMethod.returnInstruction();

        // Generate the static initializer
        final CodeAttribute clinit = asm.addMethod(Modifier.STATIC, "<clinit>", "V").getCodeAttribute();
        clinit.iconst(ids.length);
        clinit.anewarray(String.class.getName());
        for (int i = 0; i < ids.length; i++) {
            clinit.dup();
            clinit.iconst(i);
            clinit.ldc(ids[i]);
            clinit.aastore();
        }
        clinit.putstatic(stubClassName, ID_FIELD_NAME, "[Ljava/lang/String;");

        int n = methodIndex; // last methodIndex + 1
        for (methodIndex = 0; methodIndex < n; methodIndex++) {
            clinit.invokestatic(stubClassName, init(methodIndex), "()V");
        }
        clinit.returnInstruction();

        return asm;
    }

    /**
     * Generates the bytecodes of a stub class for a given interface.
     *
     * @param interfaceAnalysis an InterfaceAnalysis instance
     *                          describing the RMI/IIOP interface to be
     *                          implemented by the stub class
     * @param superclass        the superclass of the stub class
     * @param stubClassName     the name of the stub class
     * @return a byte array with the generated bytecodes.
     */
    private static ClassFile makeCode(InterfaceAnalysis interfaceAnalysis,
                                   Class<?> superclass, String stubClassName) {

        ClassFile code = generateCode(interfaceAnalysis, superclass, stubClassName);
        //try {
        //   String fname = stubClassName;
        //   fname = fname.substring(1 + fname.lastIndexOf('.')) + ".class";
        //   fname = "/tmp/" + fname;
        //   java.io.OutputStream cf = new java.io.FileOutputStream(fname);
        //   cf.write(code);
        //   cf.close();
        //   System.err.println("wrote " + fname);
        //}
        //catch(java.io.IOException ee) {
        //}
        return code;
    }

    // Public method ----------------------------------------------------------

    /**
     * Generates the bytecodes of a stub class for a given interface.
     *
     * @param intf          RMI/IIOP interface to be implemented by the
     *                      stub class
     * @param stubClassName the name of the stub class
     * @return a byte array with the generated bytecodes;
     */
    public static ClassFile compile(Class<?> intf, String stubClassName) {
        InterfaceAnalysis interfaceAnalysis = null;

        try {
            interfaceAnalysis = InterfaceAnalysis.getInterfaceAnalysis(intf);
        } catch (RMIIIOPViolationException e) {
            throw EjbLogger.ROOT_LOGGER.rmiIiopVoliation(e.getLocalizedMessage());
        }
        return makeCode(interfaceAnalysis, DynamicIIOPStub.class, stubClassName);
    }

    public Class<?> compileToClass(Class<?> intf, String stubClassName) {
        return compile(intf, stubClassName).define(intf.getClassLoader(), intf.getProtectionDomain());
    }

}


/*
 * JBoss, Home of Professional Open Source.
 * Copyright 2015, Red Hat, Inc., and individual contributors
 * as indicated by the @author tags. See the copyright.txt file in the
 * distribution for a full listing of individual contributors.
 *
 * This is free software; you can redistribute it and/or modify it
 * under the terms of the GNU Lesser General Public License as
 * published by the Free Software Foundation; either version 2.1 of
 * the License, or (at your option) any later version.
 *
 * This software is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this software; if not, write to the Free
 * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
 * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
 */

package org.wildfly.extension.batch.jberet.deployment;

import java.io.IOException;
import java.io.InputStream;
import java.security.AccessController;
import java.security.PrivilegedAction;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.ServiceLoader;
import java.util.Set;

import javax.xml.stream.XMLResolver;
import javax.xml.stream.XMLStreamException;

import org.jberet.job.model.Job;
import org.jberet.job.model.JobParser;
import org.jberet.spi.JobXmlResolver;
import org.jboss.as.ee.structure.DeploymentType;
import org.jboss.as.ee.structure.DeploymentTypeMarker;
import org.jboss.as.server.deployment.Attachments;
import org.jboss.as.server.deployment.DeploymentUnit;
import org.jboss.as.server.deployment.DeploymentUnitProcessingException;
import org.jboss.as.server.deployment.SubDeploymentMarker;
import org.jboss.as.server.deployment.module.ResourceRoot;
import org.jboss.modules.Module;
import org.jboss.vfs.VirtualFile;
import org.jboss.vfs.VirtualFileFilter;
import org.wildfly.extension.batch.jberet._private.BatchLogger;
import org.wildfly.security.manager.WildFlySecurityManager;

/**
 * A {@linkplain JobXmlResolver job XML resolver} for WildFly. A deployments resolvers are loaded via a
 * {@link ServiceLoader} and processed before XML found in the deployment itself.
 *
 * @author James R. Perkins
 */
public class WildFlyJobXmlResolver implements JobXmlResolver {

    private final Set<JobXmlResolver> jobXmlResolvers;
    private final Map<String, String> jobXmlNames;
    private final Map<String, VirtualFile> jobXmlFiles;
    private final Map<String, Set<String>> jobNames;

    private WildFlyJobXmlResolver(final Map<String, VirtualFile> jobXmlFiles) {
        jobXmlNames = new LinkedHashMap<>();
        jobXmlResolvers = new LinkedHashSet<>();
        jobNames = new LinkedHashMap<>();
        this.jobXmlFiles = jobXmlFiles;
    }

    /**
     * Creates the {@linkplain JobXmlResolver resolver} for the deployment inheriting any visible resolvers and job XML
     * files from dependencies.
     *
     * @param deploymentUnit the deployment to process
     *
     * @return the resolve
     *
     * @throws DeploymentUnitProcessingException if an error occurs processing the deployment
     */
    public static WildFlyJobXmlResolver forDeployment(final DeploymentUnit deploymentUnit) throws DeploymentUnitProcessingException {
        // If this deployment unit already has a resolver, just use it
        if (deploymentUnit.hasAttachment(BatchAttachments.JOB_XML_RESOLVER)) {
            return deploymentUnit.getAttachment(BatchAttachments.JOB_XML_RESOLVER);
        }
        // Get the module for it's class loader
        final Module module = deploymentUnit.getAttachment(Attachments.MODULE);
        final ClassLoader classLoader = module.getClassLoader();
        WildFlyJobXmlResolver resolver;
        // If we're an EAR we need to skip sub-deployments as they'll be process later, however all sub-deployments have
        // access to the EAR/lib directory so those resources need to be processed
        if (DeploymentTypeMarker.isType(DeploymentType.EAR, deploymentUnit)) {
            // Create a new WildFlyJobXmlResolver without jobs from sub-deployments as they'll be processed later
            final List<ResourceRoot> resources = new ArrayList<>();
            for (ResourceRoot r : deploymentUnit.getAttachmentList(Attachments.RESOURCE_ROOTS)) {
                if (! SubDeploymentMarker.isSubDeployment(r)) {
                    resources.add(r);
                }
            }
            resolver = create(classLoader, resources);
            deploymentUnit.putAttachment(BatchAttachments.JOB_XML_RESOLVER, resolver);
        } else {
            // Create a new resolver for this deployment
            if (deploymentUnit.hasAttachment(Attachments.RESOURCE_ROOTS)) {
                resolver = create(classLoader, deploymentUnit.getAttachmentList(Attachments.RESOURCE_ROOTS));
            } else {
                resolver = create(classLoader, Collections.singletonList(deploymentUnit.getAttachment(Attachments.DEPLOYMENT_ROOT)));
            }
            deploymentUnit.putAttachment(BatchAttachments.JOB_XML_RESOLVER, resolver);
            // Process all accessible sub-deployments
            final List<DeploymentUnit> accessibleDeployments = deploymentUnit.getAttachmentList(Attachments.ACCESSIBLE_SUB_DEPLOYMENTS);
            for (DeploymentUnit subDeployment : accessibleDeployments) {
                // Skip our self
                if (deploymentUnit.equals(subDeployment)) {
                    continue;
                }
                if (subDeployment.hasAttachment(BatchAttachments.JOB_XML_RESOLVER)) {
                    final WildFlyJobXmlResolver toCopy = subDeployment.getAttachment(BatchAttachments.JOB_XML_RESOLVER);
                    WildFlyJobXmlResolver.merge(resolver, toCopy);
                } else {
                    // We need to create a resolver for the sub-deployment and merge the two
                    final WildFlyJobXmlResolver toCopy = forDeployment(subDeployment);
                    subDeployment.putAttachment(BatchAttachments.JOB_XML_RESOLVER, toCopy);
                    WildFlyJobXmlResolver.merge(resolver, toCopy);
                }
            }
        }
        return resolver;
    }

    @Override
    public InputStream resolveJobXml(final String jobXml, final ClassLoader classLoader) throws IOException {
        if (jobXmlFiles.isEmpty() && jobXmlResolvers.isEmpty()) {
            return null;
        }
        for (JobXmlResolver resolver : jobXmlResolvers) {
            final InputStream in = resolver.resolveJobXml(jobXml, classLoader);
            if (in != null) {
                return in;
            }
        }
        final VirtualFile file = jobXmlFiles.get(jobXml);
        if (file == null) {
            return null;
        }
        if (WildFlySecurityManager.isChecking()) {
            return AccessController.doPrivileged((PrivilegedAction<InputStream>) () -> {
                try {
                    return file.openStream();
                } catch (IOException e) {
                    throw new RuntimeException(e);
                }
            });
        }
        return file.openStream();
    }

    @Override
    public Collection<String> getJobXmlNames(final ClassLoader classLoader) {
        return new ArrayList<>(jobXmlNames.keySet());
    }

    @Override
    public String resolveJobName(final String jobXml, final ClassLoader classLoader) {
        return jobXmlNames.get(jobXml);
    }

    /**
     * Validates whether or not the job name exists for this deployment.
     *
     * @param jobName the job name to check
     *
     * @return {@code true} if the job exists, otherwise {@code false}
     */
    boolean isValidJobName(final String jobName) {
        return jobNames.containsKey(jobName);
    }

    /**
     * Returns the job XML file names which contain the job name.
     *
     * @param jobName the job name to find the job XML files for
     *
     * @return the set of job XML files the job can be run from
     */
    Set<String> getJobXmlNames(final String jobName) {
        if (jobNames.containsKey(jobName)) {
            return Collections.unmodifiableSet(jobNames.get(jobName));
        }
        return Collections.emptySet();
    }

    /**
     * Returns all the job names available from this resolver.
     *
     * @return the job names available from this resolver
     */
    Set<String> getJobNames() {
        return new LinkedHashSet<>(jobNames.keySet());
    }

    /**
     * Validates whether or not the job XML descriptor exists for this deployment.
     *
     * @param jobXmlName the job XML descriptor name
     *
     * @return {@code true} if the job XML descriptor exists for this deployment, otherwise {@code false}
     */
    boolean isValidJobXmlName(final String jobXmlName) {
        return jobXmlNames.containsKey(jobXmlName);
    }

    private static WildFlyJobXmlResolver create(final ClassLoader classLoader, final List<ResourceRoot> resources) throws DeploymentUnitProcessingException {
        final Map<String, VirtualFile> foundJobXmlFiles = new LinkedHashMap<>();
        for (ResourceRoot r : resources) {
            final VirtualFile root = r.getRoot();
            try {
                addJobXmlFiles(foundJobXmlFiles, root.getChild(DEFAULT_PATH));
            } catch (IOException e) {
                throw BatchLogger.LOGGER.errorProcessingBatchJobsDir(e);
            }
        }

        final WildFlyJobXmlResolver jobXmlResolver = new WildFlyJobXmlResolver(foundJobXmlFiles);
        // Initialize this instance
        jobXmlResolver.init(classLoader);
        return jobXmlResolver;
    }

    private static void addJobXmlFiles(final Map<String, VirtualFile> foundJobXmlFiles, final VirtualFile jobsDir) throws IOException {
        if (jobsDir != null && jobsDir.exists()) {
            // We may have some job XML files
            final Map<String, VirtualFile> xmlFiles = new HashMap<>();
            for (VirtualFile f : jobsDir.getChildren(JobXmlFilter.INSTANCE)) {
                if (xmlFiles.put(f.getName(), f) != null) {
                    throw new IllegalStateException("Duplicate key");
                }
            }
            foundJobXmlFiles.putAll(xmlFiles);
        }
    }

    private static void merge(final WildFlyJobXmlResolver target, final WildFlyJobXmlResolver toCopy) {
        for (Map.Entry<String, Set<String>> entry : toCopy.jobNames.entrySet()) {
            if (target.jobNames.containsKey(entry.getKey())) {
                target.jobNames.get(entry.getKey()).addAll(entry.getValue());
            } else {
                target.jobNames.put(entry.getKey(), entry.getValue());
            }
        }
        toCopy.jobXmlNames.forEach(target.jobXmlNames::putIfAbsent);
        toCopy.jobXmlFiles.forEach(target.jobXmlFiles::putIfAbsent);
        target.jobXmlResolvers.addAll(toCopy.jobXmlResolvers);
    }

    /**
     * Initializes the state of an instance
     */
    private void init(final ClassLoader classLoader) {
        // Load the user defined resolvers
        for (JobXmlResolver resolver : ServiceLoader.load(JobXmlResolver.class, classLoader)) {
            jobXmlResolvers.add(resolver);
            for (String jobXml : resolver.getJobXmlNames(classLoader)) {
                addJob(jobXml, resolver.resolveJobName(jobXml, classLoader));
            }
        }

        // Load the default names
        for (Map.Entry<String, VirtualFile> entry : jobXmlFiles.entrySet()) {
            try {
                // Parsing the entire job XML seems excessive to just get the job name. There are two reasons for this:
                //  1) If an error occurs during parsing there's no real need to consider this a valid job
                //  2) Using the implementation parser seems less error prone for future-proofing
                final Job job = JobParser.parseJob(entry.getValue().openStream(), classLoader, new XMLResolver() {
                    // this is essentially what JBeret does, but it's ugly. JBeret might need an API to handle this
                    @Override
                    public Object resolveEntity(final String publicID, final String systemID, final String baseURI, final String namespace) throws XMLStreamException {
                        try {
                            return (jobXmlFiles.containsKey(systemID) ? jobXmlFiles.get(systemID).openStream() : null);
                        } catch (IOException e) {
                            throw new XMLStreamException(e);
                        }
                    }
                });
                addJob(entry.getKey(), job.getId());
            } catch (XMLStreamException | IOException e) {
                // Report the possible error as we don't want to fail the deployment. The job may never be run.
                BatchLogger.LOGGER.invalidJobXmlFile(entry.getKey());
            }
        }
    }

    private void addJob(final String jobXmlName, final String jobName) {
        jobXmlNames.put(jobXmlName, jobName);
        final Set<String> xmlDescriptors = jobNames.computeIfAbsent(jobName, s -> new LinkedHashSet<>());
        xmlDescriptors.add(jobXmlName);
    }

    private static class JobXmlFilter implements VirtualFileFilter {

        static final JobXmlFilter INSTANCE = new JobXmlFilter();

        @Override
        public boolean accepts(final VirtualFile file) {
            return file.isFile() && file.getName().endsWith(".xml");
        }
    }
}


/*
 * JBoss, Home of Professional Open Source.
 * Copyright 2017, Red Hat, Inc., and individual contributors
 * as indicated by the @author tags. See the copyright.txt file in the
 * distribution for a full listing of individual contributors.
 *
 * This is free software; you can redistribute it and/or modify it
 * under the terms of the GNU Lesser General Public License as
 * published by the Free Software Foundation; either version 2.1 of
 * the License, or (at your option) any later version.
 *
 * This software is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this software; if not, write to the Free
 * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
 * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
 */

package org.wildfly.extension.undertow.security;

import io.undertow.security.idm.Account;
import io.undertow.server.HttpServerExchange;
import io.undertow.servlet.api.AuthorizationManager;
import io.undertow.servlet.api.Deployment;
import io.undertow.servlet.api.SecurityRoleRef;
import io.undertow.servlet.api.ServletInfo;
import io.undertow.servlet.api.SingleConstraintMatch;
import io.undertow.servlet.api.TransportGuaranteeType;
import io.undertow.servlet.handlers.ServletRequestContext;
import org.jboss.as.core.security.SimplePrincipal;
import org.jboss.security.SecurityContext;
import org.jboss.security.authorization.ResourceKeys;
import org.jboss.security.javaee.AbstractWebAuthorizationHelper;
import org.jboss.security.javaee.SecurityHelperFactory;
import org.wildfly.extension.undertow.logging.UndertowLogger;

import javax.security.auth.Subject;
import javax.security.jacc.PolicyContext;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.security.Principal;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

/**
 * @author Stuart Douglas
 */
public class JbossAuthorizationManager implements AuthorizationManager {

    private final AuthorizationManager delegate;

    public JbossAuthorizationManager(AuthorizationManager delegate) {
        this.delegate = delegate;
    }

    @Override
    public boolean isUserInRole(String role, Account account, ServletInfo servletInfo, HttpServletRequest request, Deployment deployment) {
        boolean authzDecision = true;
        boolean baseDecision = delegate.isUserInRole(role, account, servletInfo, request, deployment);
        // if the RealmBase check has passed, then we can go to authz framework
        if (baseDecision) {
            String servletName = servletInfo.getName();
            String roleName = role;
            List<SecurityRoleRef> roleRefs = servletInfo.getSecurityRoleRefs();
            if (roleRefs != null) {
                for (SecurityRoleRef ref : roleRefs) {
                    if (ref.getLinkedRole().equals(role)) {
                        roleName = ref.getRole();
                        break;
                    }
                }
            }

            SecurityContext sc = SecurityActions.getSecurityContext();
            AbstractWebAuthorizationHelper helper = null;
            try {
                helper = SecurityHelperFactory.getWebAuthorizationHelper(sc);
            } catch (Exception e) {
                UndertowLogger.ROOT_LOGGER.noAuthorizationHelper(e);
                return false;

            }
            Subject callerSubject = sc.getUtil().getSubject();
            //if (callerSubject == null) {
            //    // During hasResourcePermission check, Catalina calls hasRole. But we have not established
            //    // a subject yet in the security context. So we will get the subject from the cached principal
            //    callerSubject = getSubjectFromRequestPrincipal(principal);
            //}

            authzDecision = helper.hasRole(roleName, account.getPrincipal(), servletName, getPrincipalRoles(account),
                    PolicyContext.getContextID(), callerSubject, new ArrayList<String>(account.getRoles()));
        }
        boolean finalDecision = baseDecision && authzDecision;
        UndertowLogger.ROOT_LOGGER.tracef("hasRole:RealmBase says: %s ::Authz framework says: %s :final= %s", baseDecision, authzDecision, finalDecision);
        //TODO: do we need audit for this?
        /*
        if (finalDecision) {
            if (!disableAudit) {
                Map<String, Object> entries = new HashMap<String, Object>();
                entries.put("Step", "hasRole");
                successAudit(principal, entries);
            }
        } else {
            if (!disableAudit) {
                Map<String, Object> entries = new HashMap<String, Object>();
                entries.put("Step", "hasRole");
                failureAudit(principal, entries);
            }
        }
        */

        return finalDecision;
    }

    private Set<Principal> getPrincipalRoles(Account account) {
        final Set<Principal> roles = new HashSet<>();
        for (String role : account.getRoles()) {
            roles.add(new SimplePrincipal(role));
        }
        return roles;
    }

    @Override
    public boolean canAccessResource(List<SingleConstraintMatch> mappedConstraints, Account account, ServletInfo servletInfo, HttpServletRequest request, Deployment deployment) {
        ServletRequestContext src = ServletRequestContext.current();
        boolean baseDecision = delegate.canAccessResource(mappedConstraints, account, servletInfo, request, deployment);
        boolean authzDecision = false;
        // if the RealmBase check has passed, then we can go to authz framework
        if (baseDecision) {
            SecurityContext sc = SecurityActions.getSecurityContext();
            Subject caller = sc.getUtil().getSubject();
            //if (caller == null) {
            //    caller = getSubjectFromRequestPrincipal(request.getPrincipal());
            //}
            Map<String, Object> contextMap = new HashMap<String, Object>();
            contextMap.put(ResourceKeys.RESOURCE_PERM_CHECK, Boolean.TRUE);
            contextMap.put("securityConstraints", mappedConstraints); //TODO? What should this be?

            AbstractWebAuthorizationHelper helper = null;
            try {
                helper = SecurityHelperFactory.getWebAuthorizationHelper(sc);
            } catch (Exception e) {
                UndertowLogger.ROOT_LOGGER.noAuthorizationHelper(e);
                return false;
            }

            ArrayList<String> roles = new ArrayList<String>();
            if(account != null) {
                roles.addAll(account.getRoles());
            }
            authzDecision = helper.checkResourcePermission(contextMap, request, src.getServletResponse(), caller, PolicyContext.getContextID(),
                    requestURI(src.getExchange()), roles);
        }
        boolean finalDecision = baseDecision && authzDecision && hasUserDataPermission(request, src.getOriginalResponse(), account, mappedConstraints);

        UndertowLogger.ROOT_LOGGER.tracef("hasResourcePermission:RealmBase says: %s ::Authz framework says: %s :final= %s", baseDecision, authzDecision, finalDecision);
        //TODO: audit?

        return finalDecision;

    }

    public boolean hasUserDataPermission(HttpServletRequest request, HttpServletResponse response, Account account, List<SingleConstraintMatch> constraints) {
        Map<String, Object> map = new HashMap<String, Object>();
        map.put("securityConstraints", constraints);
        map.put(ResourceKeys.USERDATA_PERM_CHECK, Boolean.TRUE);

        SecurityContext sc = SecurityActions.getSecurityContext();
        AbstractWebAuthorizationHelper helper = null;
        try {
            helper = SecurityHelperFactory.getWebAuthorizationHelper(sc);
        } catch (Exception e) {
            UndertowLogger.ROOT_LOGGER.noAuthorizationHelper(e);
            return false;
        }

        Subject callerSubject = sc.getUtil().getSubject();
        // JBAS-6419:CallerSubject has no bearing on the user data permission check
        if (callerSubject == null) {
            callerSubject = new Subject();
        }

        ArrayList<String> roles = new ArrayList<String>();
        if(account != null) {
            roles.addAll(account.getRoles());
        }
        boolean ok = helper.hasUserDataPermission(map, request, response, PolicyContext.getContextID(), callerSubject,
                roles);

        //If the status of the response has already been changed (it is different from the default Response.SC_OK) we should not attempt to change it.
        if (!ok && response.getStatus() == HttpServletResponse.SC_OK) {
            try {
                response.sendError(HttpServletResponse.SC_FORBIDDEN);
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
        }
        return ok;
    }

    @Override
    public TransportGuaranteeType transportGuarantee(TransportGuaranteeType currentConnectionGuarantee, TransportGuaranteeType configuredRequiredGuarantee, HttpServletRequest request) {
        return delegate.transportGuarantee(currentConnectionGuarantee, configuredRequiredGuarantee, request);
    }

    /**
     * Get the canonical request URI from the request mapping data requestPath
     *
     * @param request
     * @return the request URI path
     */
    protected String requestURI(HttpServerExchange request) {
        String uri = request.getRelativePath();
        if (uri == null || uri.equals("/")) {
            uri = "";
        }
        return uri;
    }

}