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

import java.io.OutputStream;
import java.security.Key;
import java.security.NoSuchAlgorithmException;
import java.security.PublicKey;
import java.security.cert.X509Certificate;
import java.util.List;

import javax.crypto.KeyGenerator;
import javax.crypto.spec.SecretKeySpec;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.stream.XMLStreamWriter;

import org.apache.wss4j.common.crypto.Crypto;
import org.apache.wss4j.common.crypto.CryptoType;
import org.apache.wss4j.common.ext.WSPasswordCallback;
import org.apache.wss4j.common.ext.WSSecurityException;
import org.apache.wss4j.stax.ext.DocumentCreatorImpl;
import org.apache.wss4j.stax.ext.WSSConstants;
import org.apache.wss4j.stax.ext.WSSSecurityProperties;
import org.apache.wss4j.stax.impl.processor.output.BinarySecurityTokenOutputProcessor;
import org.apache.wss4j.stax.impl.processor.output.CustomTokenOutputProcessor;
import org.apache.wss4j.stax.impl.processor.output.DerivedKeyTokenOutputProcessor;
import org.apache.wss4j.stax.impl.processor.output.EncryptEndingOutputProcessor;
import org.apache.wss4j.stax.impl.processor.output.EncryptOutputProcessor;
import org.apache.wss4j.stax.impl.processor.output.EncryptedKeyOutputProcessor;
import org.apache.wss4j.stax.impl.processor.output.ReferenceListOutputProcessor;
import org.apache.wss4j.stax.impl.processor.output.SAMLTokenOutputProcessor;
import org.apache.wss4j.stax.impl.processor.output.SecurityContextTokenOutputProcessor;
import org.apache.wss4j.stax.impl.processor.output.SecurityHeaderOutputProcessor;
import org.apache.wss4j.stax.impl.processor.output.SecurityHeaderReorderProcessor;
import org.apache.wss4j.stax.impl.processor.output.SignatureConfirmationOutputProcessor;
import org.apache.wss4j.stax.impl.processor.output.TimestampOutputProcessor;
import org.apache.wss4j.stax.impl.processor.output.UsernameTokenOutputProcessor;
import org.apache.wss4j.stax.impl.processor.output.WSSSignatureOutputProcessor;
import org.apache.wss4j.stax.impl.securityToken.KerberosClientSecurityToken;
import org.apache.wss4j.stax.securityToken.WSSecurityTokenConstants;
import org.apache.wss4j.stax.utils.WSSUtils;
import org.apache.xml.security.exceptions.XMLSecurityException;
import org.apache.xml.security.stax.config.JCEAlgorithmMapper;
import org.apache.xml.security.stax.ext.OutboundSecurityContext;
import org.apache.xml.security.stax.ext.OutputProcessor;
import org.apache.xml.security.stax.ext.SecurityContext;
import org.apache.xml.security.stax.ext.XMLSecurityConstants;
import org.apache.xml.security.stax.impl.DocumentContextImpl;
import org.apache.xml.security.stax.impl.OutboundSecurityContextImpl;
import org.apache.xml.security.stax.impl.OutputProcessorChainImpl;
import org.apache.xml.security.stax.impl.XMLSecurityStreamWriter;
import org.apache.xml.security.stax.impl.processor.output.FinalOutputProcessor;
import org.apache.xml.security.stax.impl.securityToken.GenericOutboundSecurityToken;
import org.apache.xml.security.stax.impl.util.IDGenerator;
import org.apache.xml.security.stax.securityEvent.SecurityEvent;
import org.apache.xml.security.stax.securityEvent.SecurityEventListener;
import org.apache.xml.security.stax.securityEvent.TokenSecurityEvent;
import org.apache.xml.security.stax.securityToken.OutboundSecurityToken;
import org.apache.xml.security.stax.securityToken.SecurityToken;
import org.apache.xml.security.stax.securityToken.SecurityTokenConstants.TokenUsage;
import org.apache.xml.security.stax.securityToken.SecurityTokenProvider;

/**
 * Outbound Streaming-WebService-Security
 * An instance of this class can be retrieved over the WSSec class
 */
public class OutboundWSSec {

    private final WSSSecurityProperties securityProperties;

    public OutboundWSSec(WSSSecurityProperties securityProperties) {
        this.securityProperties = securityProperties;
    }

    /**
     * This method is the entry point for the incoming security-engine.
     * Hand over a outputStream and use the returned XMLStreamWriter for further processing
     *
     * @param outputStream The original outputStream
     * @return A new XMLStreamWriter which does transparently the security processing.
     * @throws WSSecurityException thrown when a Security failure occurs
     */
    public XMLStreamWriter processOutMessage(
            OutputStream outputStream, String encoding,
            List<SecurityEvent> requestSecurityEvents) throws WSSecurityException {
        return processOutMessage(outputStream, encoding, requestSecurityEvents, null);
    }

    /**
     * This method is the entry point for the incoming security-engine.
     * Hand over the original XMLStreamWriter and use the returned one for further processing
     *
     * @param xmlStreamWriter The original xmlStreamWriter
     * @return A new XMLStreamWriter which does transparently the security processing.
     * @throws WSSecurityException thrown when a Security failure occurs
     */
    public XMLStreamWriter processOutMessage(
            XMLStreamWriter xmlStreamWriter, String encoding,
            List<SecurityEvent> requestSecurityEvents) throws WSSecurityException {
        return processOutMessage(xmlStreamWriter, encoding, requestSecurityEvents, null);
    }

    /**
     * This method is the entry point for the incoming security-engine.
     * Hand over a outputstream and use the returned XMLStreamWriter for further processing
     *
     * @param outputStream The original outputStream
     * @return A new XMLStreamWriter which does transparently the security processing.
     * @throws WSSecurityException thrown when a Security failure occurs
     */
    public XMLStreamWriter processOutMessage(
            OutputStream outputStream, String encoding, List<SecurityEvent> requestSecurityEvents,
            SecurityEventListener securityEventListener) throws WSSecurityException {
        final OutboundSecurityContextImpl outboundSecurityContext = new OutboundSecurityContextImpl();
        outboundSecurityContext.putList(SecurityEvent.class, requestSecurityEvents);
        outboundSecurityContext.addSecurityEventListener(securityEventListener);
        return processOutMessage(outputStream, encoding, outboundSecurityContext);
    }

    /**
     * This method is the entry point for the incoming security-engine.
     * Hand over the original XMLStreamWriter and use the returned one for further processing
     *
     * @param xmlStreamWriter The original outputStream
     * @return A new XMLStreamWriter which does transparently the security processing.
     * @throws WSSecurityException thrown when a Security failure occurs
     */
    public XMLStreamWriter processOutMessage(
            XMLStreamWriter xmlStreamWriter, String encoding, List<SecurityEvent> requestSecurityEvents,
            SecurityEventListener securityEventListener) throws WSSecurityException {
        final OutboundSecurityContextImpl outboundSecurityContext = new OutboundSecurityContextImpl();
        outboundSecurityContext.putList(SecurityEvent.class, requestSecurityEvents);
        outboundSecurityContext.addSecurityEventListener(securityEventListener);
        return processOutMessage((Object) xmlStreamWriter, encoding, outboundSecurityContext);
    }

    /**
     * This method is the entry point for the incoming security-engine.
     * Hand over the original XMLStreamWriter and use the returned one for further processing
     *
     * @param xmlStreamWriter The original outputStream
     * @return A new XMLStreamWriter which does transparently the security processing.
     * @throws WSSecurityException thrown when a Security failure occurs
     */
    public XMLStreamWriter processOutMessage(
            XMLStreamWriter xmlStreamWriter, String encoding, OutboundSecurityContext outbounSecurityContext)
                throws WSSecurityException {
        return processOutMessage((Object) xmlStreamWriter, encoding, outbounSecurityContext);
    }

    public XMLStreamWriter processOutMessage(
            Object output, String encoding, OutboundSecurityContext outboundSecurityContext
        ) throws WSSecurityException {

        final DocumentContextImpl documentContext = new DocumentContextImpl();
        documentContext.setEncoding(encoding);

        OutputProcessorChainImpl outputProcessorChain = new OutputProcessorChainImpl(outboundSecurityContext, documentContext);

        try {
            final SecurityHeaderOutputProcessor securityHeaderOutputProcessor = new SecurityHeaderOutputProcessor();
            initializeOutputProcessor(outputProcessorChain, securityHeaderOutputProcessor, null, -1);

            ConfiguredAction configuredAction = configureActions(outputProcessorChain);

            // Set up appropriate keys
            if (configuredAction.signatureAction) {
                setupSignatureKey(outputProcessorChain, securityProperties, configuredAction.signedSAML);
            }
            if (configuredAction.encryptionAction) {
                setupEncryptionKey(outputProcessorChain, securityProperties);
            }
            if (configuredAction.kerberos) {
                setupKerberosKey(outputProcessorChain, securityProperties,
                                 configuredAction.signatureKerberos, configuredAction.encryptionKerberos);
            }
            if (configuredAction.derivedSignature) {
                String id =
                    outputProcessorChain.getSecurityContext().get(WSSConstants.PROP_USE_THIS_TOKEN_ID_FOR_SIGNATURE);
                setDerivedIdentifier(outputProcessorChain, id);
            }
            if (configuredAction.derivedEncryption) {
                String id =
                    outputProcessorChain.getSecurityContext().get(WSSConstants.PROP_USE_THIS_TOKEN_ID_FOR_ENCRYPTED_KEY);
                if (id == null) {
                    // Maybe not encrypting the key here...
                    id = outputProcessorChain.getSecurityContext().get(WSSConstants.PROP_USE_THIS_TOKEN_ID_FOR_ENCRYPTION);
                }
                setDerivedIdentifier(outputProcessorChain, id);
            }

            final SecurityHeaderReorderProcessor securityHeaderReorderProcessor = new SecurityHeaderReorderProcessor();
            initializeOutputProcessor(outputProcessorChain, securityHeaderReorderProcessor, null, -1);

            if (output instanceof OutputStream) {
                final FinalOutputProcessor finalOutputProcessor = new FinalOutputProcessor((OutputStream) output, encoding);
                initializeOutputProcessor(outputProcessorChain, finalOutputProcessor, null, -1);

            } else if (output instanceof XMLStreamWriter) {
                final FinalOutputProcessor finalOutputProcessor = new FinalOutputProcessor((XMLStreamWriter) output);
                initializeOutputProcessor(outputProcessorChain, finalOutputProcessor, null, -1);

            } else {
                throw new IllegalArgumentException(output + " is not supported as output");
            }
        } catch (XMLSecurityException e) {
            throw new WSSecurityException(WSSecurityException.ErrorCode.FAILURE, e);
        }
        return new XMLSecurityStreamWriter(outputProcessorChain);
    }

    private void initializeOutputProcessor(
            OutputProcessorChainImpl outputProcessorChain, OutputProcessor outputProcessor,
            XMLSecurityConstants.Action action, int actionOrder) throws XMLSecurityException {
        if (actionOrder > -1) {
            outputProcessor.addAfterProcessor(TimestampOutputProcessor.class);
            outputProcessor.addAfterProcessor(UsernameTokenOutputProcessor.class);
            outputProcessor.addAfterProcessor(SignatureConfirmationOutputProcessor.class);
            outputProcessor.addAfterProcessor(CustomTokenOutputProcessor.class);
            outputProcessor.addAfterProcessor(BinarySecurityTokenOutputProcessor.class);
            outputProcessor.addAfterProcessor(SAMLTokenOutputProcessor.class);
        }
        outputProcessor.setXMLSecurityProperties(securityProperties);
        outputProcessor.setAction(action, actionOrder);
        outputProcessor.init(outputProcessorChain);
    }

    private void setupSignatureKey(
        OutputProcessorChainImpl outputProcessorChain,
        WSSSecurityProperties securityProperties,
        boolean signedSAML
    ) throws XMLSecurityException {
        final String signatureAlgorithm = securityProperties.getSignatureAlgorithm();

        GenericOutboundSecurityToken securityToken =
            getOutboundSecurityToken(outputProcessorChain, WSSConstants.PROP_USE_THIS_TOKEN_ID_FOR_SIGNATURE);
        // First off, see if we have a supplied token with the correct keys for
        // (a)symmetric signature
        if (securityToken != null && signatureAlgorithm != null) {
            if (signatureAlgorithm.contains("hmac-sha")
                && securityToken.getSecretKey(signatureAlgorithm) != null) {
                return;
            } else if (!signatureAlgorithm.contains("hmac-sha") && securityToken.getX509Certificates() != null) {
                if (securityToken.getSecretKey(signatureAlgorithm) != null) {
                    return;
                } else {
                    // We have certs but no private key set. Use the CallbackHandler
                    Key key =
                        securityProperties.getSignatureCrypto().getPrivateKey(
                            securityToken.getX509Certificates()[0], securityProperties.getCallbackHandler()
                        );
                    securityToken.setSecretKey(signatureAlgorithm, key);
                    return;
                }
            }
        }

        // We have no supplied key. So use the PasswordCallback to get a secret key or password
        String alias = securityProperties.getSignatureUser();
        WSPasswordCallback pwCb = new WSPasswordCallback(alias, WSPasswordCallback.SIGNATURE);
            WSSUtils.doPasswordCallback(securityProperties.getCallbackHandler(), pwCb);

        String password = pwCb.getPassword();
        byte[] secretKey = pwCb.getKey();
        Key key = null;
        X509Certificate[] x509Certificates = null;
        try {
            if (password != null && securityProperties.getSignatureCrypto() != null) {
                key = securityProperties.getSignatureCrypto().getPrivateKey(alias, password);
                CryptoType cryptoType = new CryptoType(CryptoType.TYPE.ALIAS);
                cryptoType.setAlias(alias);
                x509Certificates = securityProperties.getSignatureCrypto().getX509Certificates(cryptoType);
                if (x509Certificates == null || x509Certificates.length == 0) {
                    throw new WSSecurityException(WSSecurityException.ErrorCode.FAILED_SIGNATURE, "noUserCertsFound",
                                                  new Object[] {alias});
                }
            } else if (secretKey != null) {
                x509Certificates = null;
                String algoFamily = JCEAlgorithmMapper.getJCEKeyAlgorithmFromURI(signatureAlgorithm);
                key = new SecretKeySpec(secretKey, algoFamily);
            } else {
                throw new WSSecurityException(WSSecurityException.ErrorCode.FAILED_SIGNATURE, "noPassword",
                                              new Object[] {alias});
            }
        } catch (WSSecurityException ex) {
            if (signedSAML && securityProperties.getSamlCallbackHandler() != null) {
                // We may get the keys we require from the SAML CallbackHandler...
                return;
            }
            throw ex;
        }

        // Create a new outbound Signature token for the generated key / cert
        final String id = IDGenerator.generateID(null);
        final GenericOutboundSecurityToken binarySecurityToken =
                new GenericOutboundSecurityToken(id, WSSecurityTokenConstants.X509V3Token, key, x509Certificates);

        // binarySecurityToken.setSha1Identifier(reference);
        final SecurityTokenProvider<OutboundSecurityToken> binarySecurityTokenProvider =
                new SecurityTokenProvider<OutboundSecurityToken>() {

            @Override
            public OutboundSecurityToken getSecurityToken() throws WSSecurityException {
                return binarySecurityToken;
            }

            @Override
            public String getId() {
                return id;
            }
        };

        outputProcessorChain.getSecurityContext().registerSecurityTokenProvider(id, binarySecurityTokenProvider);
        outputProcessorChain.getSecurityContext().put(WSSConstants.PROP_USE_THIS_TOKEN_ID_FOR_SIGNATURE, id);
    }

    private void setupEncryptionKey(
        OutputProcessorChainImpl outputProcessorChain,
        WSSSecurityProperties securityProperties
    ) throws XMLSecurityException {
        final String symmetricEncryptionAlgorithm = securityProperties.getEncryptionSymAlgorithm();

        // First check to see if a Symmetric key is available
        GenericOutboundSecurityToken securityToken =
            getOutboundSecurityToken(outputProcessorChain, WSSConstants.PROP_USE_THIS_TOKEN_ID_FOR_ENCRYPTION);
        if (securityToken == null || securityToken.getSecretKey(symmetricEncryptionAlgorithm) == null) {
            //prepare the symmetric session key for all encryption parts
            String keyAlgorithm = JCEAlgorithmMapper.getJCEKeyAlgorithmFromURI(securityProperties.getEncryptionSymAlgorithm());
            KeyGenerator keyGen;
            try {
                keyGen = KeyGenerator.getInstance(keyAlgorithm);
            } catch (NoSuchAlgorithmException e) {
                throw new WSSecurityException(WSSecurityException.ErrorCode.FAILURE, e);
            }
            //the sun JCE provider expects the real key size for 3DES (112 or 168 bit)
            //whereas bouncy castle expects the block size of 128 or 192 bits
            if (keyAlgorithm.contains("AES")) {
                int keyLength = JCEAlgorithmMapper.getKeyLengthFromURI(securityProperties.getEncryptionSymAlgorithm());
                keyGen.init(keyLength);
            }

            final Key symmetricKey = keyGen.generateKey();
            final String symmId = IDGenerator.generateID(null);

            final GenericOutboundSecurityToken symmetricSecurityToken =
                new GenericOutboundSecurityToken(symmId, WSSecurityTokenConstants.EncryptedKeyToken, symmetricKey);
            securityToken = symmetricSecurityToken;
            final SecurityTokenProvider<OutboundSecurityToken> securityTokenProvider =
                new SecurityTokenProvider<OutboundSecurityToken>() {

                @Override
                public OutboundSecurityToken getSecurityToken() throws XMLSecurityException {
                    return symmetricSecurityToken;
                }

                @Override
                public String getId() {
                    return symmId;
                }
            };

            outputProcessorChain.getSecurityContext().registerSecurityTokenProvider(symmId, securityTokenProvider);
            outputProcessorChain.getSecurityContext().put(WSSConstants.PROP_USE_THIS_TOKEN_ID_FOR_ENCRYPTION, symmId);
        }

        if (!securityProperties.isEncryptSymmetricEncryptionKey()) {
            // No EncryptedKey Token required here, so return
            return;
        }

        // Set up a security token with the certs required to encrypt the symmetric key
        X509Certificate[] x509Certificates = null;
        PublicKey publicKey = null;
        if (securityProperties.isUseReqSigCertForEncryption()) {
            X509Certificate x509Certificate = getReqSigCert(outputProcessorChain.getSecurityContext());
            if (x509Certificate == null) {
                publicKey = getReqSigPublicKey(outputProcessorChain.getSecurityContext());
                if (publicKey == null) {
                    throw new WSSecurityException(WSSecurityException.ErrorCode.FAILED_ENCRYPTION, "noCert");
                }
            } else {
                x509Certificates = new X509Certificate[1];
                x509Certificates[0] = x509Certificate;
            }
        } else if (securityProperties.getEncryptionUseThisCertificate() != null) {
            x509Certificates = new X509Certificate[1];
            x509Certificates[0] = securityProperties.getEncryptionUseThisCertificate();
        } else {
            CryptoType cryptoType = new CryptoType(CryptoType.TYPE.ALIAS);
            cryptoType.setAlias(securityProperties.getEncryptionUser());
            Crypto crypto = securityProperties.getEncryptionCrypto();
            x509Certificates = crypto.getX509Certificates(cryptoType);
            if (x509Certificates == null || x509Certificates.length == 0) {
                throw new WSSecurityException(WSSecurityException.ErrorCode.FAILED_ENCRYPTION, "noUserCertsFound",
                                              new Object[] {securityProperties.getEncryptionUser(), "encryption"});
            }
        }

        // Check for Revocation
        if (securityProperties.isEnableRevocation() && x509Certificates != null) {
            Crypto crypto = securityProperties.getEncryptionCrypto();
            crypto.verifyTrust(x509Certificates, true, null, null);
        }

        // Create a new outbound EncryptedKey token for the cert
        final String id = IDGenerator.generateID(null);
        final GenericOutboundSecurityToken encryptedKeyToken =
            new GenericOutboundSecurityToken(id, WSSecurityTokenConstants.X509V3Token, publicKey, x509Certificates);

        encryptedKeyToken.addWrappedToken(securityToken);
        securityToken.setKeyWrappingToken(encryptedKeyToken);

        // binarySecurityToken.setSha1Identifier(reference);
        final SecurityTokenProvider<OutboundSecurityToken> encryptedKeyTokenProvider =
            new SecurityTokenProvider<OutboundSecurityToken>() {

            @Override
            public OutboundSecurityToken getSecurityToken() throws WSSecurityException {
                return encryptedKeyToken;
            }

            @Override
            public String getId() {
                return id;
            }
        };

        outputProcessorChain.getSecurityContext().registerSecurityTokenProvider(id, encryptedKeyTokenProvider);
        outputProcessorChain.getSecurityContext().put(WSSConstants.PROP_USE_THIS_TOKEN_ID_FOR_ENCRYPTED_KEY, id);
    }

    private void setupKerberosKey(
        OutputProcessorChainImpl outputProcessorChain,
        WSSSecurityProperties securityProperties,
        boolean signature,
        boolean encryption
    ) throws XMLSecurityException {
        GenericOutboundSecurityToken securityToken =
            getOutboundSecurityToken(outputProcessorChain, WSSConstants.PROP_USE_THIS_TOKEN_ID_FOR_KERBEROS);
        String kerberosId = null;
        // First off, see if we have a supplied token
        if (securityToken == null) {
            // If not then generate a new key
            final String id = IDGenerator.generateID(null);
            kerberosId = id;
            final KerberosClientSecurityToken kerberosClientSecurityToken =
                    new KerberosClientSecurityToken(
                        securityProperties.getCallbackHandler(), id
                    );

            final SecurityTokenProvider<OutboundSecurityToken> kerberosSecurityTokenProvider =
                    new SecurityTokenProvider<OutboundSecurityToken>() {

                @Override
                public OutboundSecurityToken getSecurityToken() throws WSSecurityException {
                    return kerberosClientSecurityToken;
                }

                @Override
                public String getId() {
                    return id;
                }
            };

            outputProcessorChain.getSecurityContext().registerSecurityTokenProvider(id, kerberosSecurityTokenProvider);
            outputProcessorChain.getSecurityContext().put(WSSConstants.PROP_USE_THIS_TOKEN_ID_FOR_KERBEROS, id);
        } else {
            kerberosId = securityToken.getId();
        }

        if (signature) {
            outputProcessorChain.getSecurityContext().put(WSSConstants.PROP_USE_THIS_TOKEN_ID_FOR_SIGNATURE, kerberosId);
        }
        if (encryption) {
            outputProcessorChain.getSecurityContext().put(WSSConstants.PROP_USE_THIS_TOKEN_ID_FOR_ENCRYPTION, kerberosId);
        }

    }

    // Return an outbound SecurityToken object for a given id (encryption/signature)
    private GenericOutboundSecurityToken getOutboundSecurityToken(
        OutputProcessorChainImpl outputProcessorChain, String id
    ) throws XMLSecurityException {
        String tokenId =
            outputProcessorChain.getSecurityContext().get(id);
        SecurityTokenProvider<OutboundSecurityToken> signatureTokenProvider = null;
        if (tokenId != null) {
            signatureTokenProvider =
                outputProcessorChain.getSecurityContext().getSecurityTokenProvider(tokenId);
            if (signatureTokenProvider != null) {
                return (GenericOutboundSecurityToken)signatureTokenProvider.getSecurityToken();
            }
        }

        return null;
    }

    private X509Certificate getReqSigCert(SecurityContext securityContext) throws XMLSecurityException {
        List<SecurityEvent> securityEventList = securityContext.getAsList(SecurityEvent.class);
        if (securityEventList != null) {
            for (int i = 0; i < securityEventList.size(); i++) {
                SecurityEvent securityEvent = securityEventList.get(i);
                if (securityEvent instanceof TokenSecurityEvent) {
                    @SuppressWarnings("unchecked")
                    TokenSecurityEvent<? extends SecurityToken> tokenSecurityEvent
                        = (TokenSecurityEvent<? extends SecurityToken>) securityEvent;
                    TokenUsage mainSig = WSSecurityTokenConstants.TOKENUSAGE_MAIN_SIGNATURE;
                    if (!tokenSecurityEvent.getSecurityToken().getTokenUsages().contains(mainSig)) {
                        continue;
                    }
                    X509Certificate[] x509Certificates = tokenSecurityEvent.getSecurityToken().getX509Certificates();
                    if (x509Certificates != null && x509Certificates.length > 0) {
                        return x509Certificates[0];
                    }
                }
            }
        }
        return null;
    }

    private PublicKey getReqSigPublicKey(SecurityContext securityContext) throws XMLSecurityException {
        List<SecurityEvent> securityEventList = securityContext.getAsList(SecurityEvent.class);
        if (securityEventList != null) {
            for (int i = 0; i < securityEventList.size(); i++) {
                SecurityEvent securityEvent = securityEventList.get(i);
                if (securityEvent instanceof TokenSecurityEvent) {
                    @SuppressWarnings("unchecked")
                    TokenSecurityEvent<? extends SecurityToken> tokenSecurityEvent
                        = (TokenSecurityEvent<? extends SecurityToken>) securityEvent;
                    TokenUsage mainSig = WSSecurityTokenConstants.TOKENUSAGE_MAIN_SIGNATURE;
                    if (!tokenSecurityEvent.getSecurityToken().getTokenUsages().contains(mainSig)) {
                        continue;
                    }
                    PublicKey publicKey = tokenSecurityEvent.getSecurityToken().getPublicKey();
                    if (publicKey != null) {
                        return publicKey;
                    }
                }
            }
        }
        return null;
    }

    private void setDerivedIdentifier(OutputProcessorChainImpl outputProcessorChain, String id) {
        WSSConstants.DerivedKeyTokenReference derivedKeyTokenReference = securityProperties.getDerivedKeyTokenReference();
            switch (derivedKeyTokenReference) {

            case DirectReference:
                outputProcessorChain.getSecurityContext().put(WSSConstants.PROP_USE_THIS_TOKEN_ID_FOR_DERIVED_KEY, id);
                break;
            case EncryptedKey:
                String symmId = outputProcessorChain.getSecurityContext().get(WSSConstants.PROP_USE_THIS_TOKEN_ID_FOR_ENCRYPTION);
                outputProcessorChain.getSecurityContext().put(WSSConstants.PROP_USE_THIS_TOKEN_ID_FOR_DERIVED_KEY, symmId);
                outputProcessorChain.getSecurityContext().put(WSSConstants.PROP_USE_THIS_TOKEN_ID_FOR_ENCRYPTED_KEY, id);
                break;
            case SecurityContextToken:
                outputProcessorChain.getSecurityContext().put(WSSConstants.PROP_USE_THIS_TOKEN_ID_FOR_SECURITYCONTEXTTOKEN, id);
                break;
            }
    }

    private ConfiguredAction configureActions(OutputProcessorChainImpl outputProcessorChain) throws XMLSecurityException {
        ConfiguredAction configuredAction = new ConfiguredAction();

        //todo some combinations are not possible atm: eg Action.SIGNATURE and Action.USERNAMETOKEN_SIGNED
        //todo they use the same signature parts

        // Check to see whether we have a derived key signature, but not encryption, using
        // an encrypted key reference (as we only want one encrypted key here...)
        boolean derivedSignatureButNotDerivedEncryption = false;
        if (securityProperties.getDerivedKeyTokenReference() == WSSConstants.DerivedKeyTokenReference.EncryptedKey) {
            for (XMLSecurityConstants.Action action : securityProperties.getActions()) {
                if (WSSConstants.SIGNATURE_WITH_DERIVED_KEY.equals(action)) {
                    derivedSignatureButNotDerivedEncryption = true;
                } else if (WSSConstants.ENCRYPTION_WITH_DERIVED_KEY.equals(action)) {
                    derivedSignatureButNotDerivedEncryption = false;
                    break;
                }
            }
        }

        int actionOrder = -1;
        for (XMLSecurityConstants.Action action : securityProperties.getActions()) {
            if (WSSConstants.TIMESTAMP.equals(action)) {
                final TimestampOutputProcessor timestampOutputProcessor = new TimestampOutputProcessor();
                initializeOutputProcessor(outputProcessorChain, timestampOutputProcessor, action, -1);
            } else if (WSSConstants.SIGNATURE.equals(action)) {
                configuredAction.signatureAction = true;
                final BinarySecurityTokenOutputProcessor binarySecurityTokenOutputProcessor =
                    new BinarySecurityTokenOutputProcessor();
                initializeOutputProcessor(outputProcessorChain, binarySecurityTokenOutputProcessor, action, -1);

                final WSSSignatureOutputProcessor signatureOutputProcessor = new WSSSignatureOutputProcessor();
                initializeOutputProcessor(outputProcessorChain, signatureOutputProcessor, action, ++actionOrder);

            } else if (WSSConstants.ENCRYPTION.equals(action)) {
                configuredAction.encryptionAction = true;
                EncryptedKeyOutputProcessor encryptedKeyOutputProcessor = null;
                ++actionOrder;
                if (securityProperties.isEncryptSymmetricEncryptionKey()) {
                    final BinarySecurityTokenOutputProcessor binarySecurityTokenOutputProcessor =
                        new BinarySecurityTokenOutputProcessor();
                    initializeOutputProcessor(outputProcessorChain, binarySecurityTokenOutputProcessor, action, -1);

                    encryptedKeyOutputProcessor = new EncryptedKeyOutputProcessor();
                    initializeOutputProcessor(outputProcessorChain, encryptedKeyOutputProcessor, action, actionOrder);
                }

                final EncryptOutputProcessor encryptOutputProcessor = new EncryptOutputProcessor();
                initializeOutputProcessor(outputProcessorChain, encryptOutputProcessor, action, actionOrder);

                if (encryptedKeyOutputProcessor == null) {
                    final ReferenceListOutputProcessor referenceListOutputProcessor = new ReferenceListOutputProcessor();
                    referenceListOutputProcessor.addAfterProcessor(EncryptEndingOutputProcessor.class);
                    initializeOutputProcessor(outputProcessorChain, referenceListOutputProcessor, action, actionOrder);
                }

            } else if (WSSConstants.USERNAMETOKEN.equals(action)) {
                final UsernameTokenOutputProcessor usernameTokenOutputProcessor = new UsernameTokenOutputProcessor();
                initializeOutputProcessor(outputProcessorChain, usernameTokenOutputProcessor, action, -1);
            } else if (WSSConstants.USERNAMETOKEN_SIGNED.equals(action)) {
                final UsernameTokenOutputProcessor usernameTokenOutputProcessor = new UsernameTokenOutputProcessor();
                initializeOutputProcessor(outputProcessorChain, usernameTokenOutputProcessor, action, -1);

                final WSSSignatureOutputProcessor signatureOutputProcessor = new WSSSignatureOutputProcessor();
                initializeOutputProcessor(outputProcessorChain, signatureOutputProcessor, action, ++actionOrder);

            } else if (WSSConstants.SIGNATURE_CONFIRMATION.equals(action)) {
                final SignatureConfirmationOutputProcessor signatureConfirmationOutputProcessor =
                        new SignatureConfirmationOutputProcessor();
                initializeOutputProcessor(outputProcessorChain, signatureConfirmationOutputProcessor, action, -1);

            } else if (WSSConstants.SIGNATURE_WITH_DERIVED_KEY.equals(action)) {
                ++actionOrder;
                if (securityProperties.getDerivedKeyTokenReference() == WSSConstants.DerivedKeyTokenReference.EncryptedKey) {
                    if (derivedSignatureButNotDerivedEncryption) {
                        final EncryptedKeyOutputProcessor encryptedKeyOutputProcessor = new EncryptedKeyOutputProcessor();
                        initializeOutputProcessor(outputProcessorChain, encryptedKeyOutputProcessor, action, actionOrder);
                    }
                    configuredAction.encryptionAction = true;
                    configuredAction.derivedEncryption = true;
                } else if (securityProperties.getDerivedKeyTokenReference()
                    == WSSConstants.DerivedKeyTokenReference.SecurityContextToken) {
                    final SecurityContextTokenOutputProcessor securityContextTokenOutputProcessor =
                            new SecurityContextTokenOutputProcessor();
                    initializeOutputProcessor(outputProcessorChain, securityContextTokenOutputProcessor, action, -1);
                    configuredAction.signatureAction = true;
                    configuredAction.derivedSignature = true;
                } else {
                    configuredAction.signatureAction = true;
                    configuredAction.derivedSignature = true;
                }

                final DerivedKeyTokenOutputProcessor derivedKeyTokenOutputProcessor = new DerivedKeyTokenOutputProcessor();
                initializeOutputProcessor(outputProcessorChain, derivedKeyTokenOutputProcessor, action, -1);

                final WSSSignatureOutputProcessor signatureOutputProcessor = new WSSSignatureOutputProcessor();
                initializeOutputProcessor(outputProcessorChain, signatureOutputProcessor, action, actionOrder);

            } else if (WSSConstants.ENCRYPTION_WITH_DERIVED_KEY.equals(action)) {
                configuredAction.encryptionAction = true;
                configuredAction.derivedEncryption = true;

                EncryptedKeyOutputProcessor encryptedKeyOutputProcessor = null;

                ++actionOrder;
                if (securityProperties.getDerivedKeyTokenReference() == WSSConstants.DerivedKeyTokenReference.EncryptedKey) {
                    encryptedKeyOutputProcessor = new EncryptedKeyOutputProcessor();
                    initializeOutputProcessor(outputProcessorChain, encryptedKeyOutputProcessor, action, actionOrder);

                } else if (securityProperties.getDerivedKeyTokenReference()
                    == WSSConstants.DerivedKeyTokenReference.SecurityContextToken) {
                    final SecurityContextTokenOutputProcessor securityContextTokenOutputProcessor =
                            new SecurityContextTokenOutputProcessor();
                    initializeOutputProcessor(outputProcessorChain, securityContextTokenOutputProcessor, action, actionOrder);
                }
                final DerivedKeyTokenOutputProcessor derivedKeyTokenOutputProcessor = new DerivedKeyTokenOutputProcessor();
                initializeOutputProcessor(outputProcessorChain, derivedKeyTokenOutputProcessor, action, actionOrder);

                final EncryptOutputProcessor encryptOutputProcessor = new EncryptOutputProcessor();
                initializeOutputProcessor(outputProcessorChain, encryptOutputProcessor, action, actionOrder);

                if (encryptedKeyOutputProcessor == null) {
                    final ReferenceListOutputProcessor referenceListOutputProcessor = new ReferenceListOutputProcessor();
                    referenceListOutputProcessor.addAfterProcessor(EncryptEndingOutputProcessor.class);
                    initializeOutputProcessor(outputProcessorChain, referenceListOutputProcessor, action, actionOrder);
                }
            } else if (WSSConstants.SAML_TOKEN_SIGNED.equals(action)) {
                configuredAction.signatureAction = true;
                configuredAction.signedSAML = true;
                final BinarySecurityTokenOutputProcessor binarySecurityTokenOutputProcessor =
                    new BinarySecurityTokenOutputProcessor();
                initializeOutputProcessor(outputProcessorChain, binarySecurityTokenOutputProcessor, action, -1);

                final SAMLTokenOutputProcessor samlTokenOutputProcessor = new SAMLTokenOutputProcessor();
                initializeOutputProcessor(outputProcessorChain, samlTokenOutputProcessor, action, -1);

                final WSSSignatureOutputProcessor signatureOutputProcessor = new WSSSignatureOutputProcessor();
                initializeOutputProcessor(outputProcessorChain, signatureOutputProcessor, action, ++actionOrder);

                if (securityProperties.getDocumentCreator() == null) {
                    try {
                        securityProperties.setDocumentCreator(new DocumentCreatorImpl());
                    } catch (ParserConfigurationException e) {
                        throw new XMLSecurityException(e);
                    }
                }

            } else if (WSSConstants.SAML_TOKEN_UNSIGNED.equals(action)) {
                final SAMLTokenOutputProcessor samlTokenOutputProcessor = new SAMLTokenOutputProcessor();
                initializeOutputProcessor(outputProcessorChain, samlTokenOutputProcessor, action, -1);

                if (securityProperties.getDocumentCreator() == null) {
                    try {
                        securityProperties.setDocumentCreator(new DocumentCreatorImpl());
                    } catch (ParserConfigurationException e) {
                        throw new XMLSecurityException(e);
                    }
                }
            } else if (WSSConstants.SIGNATURE_WITH_KERBEROS_TOKEN.equals(action)) {
                configuredAction.kerberos = true;
                configuredAction.signatureKerberos = true;
                final BinarySecurityTokenOutputProcessor kerberosTokenOutputProcessor =
                        new BinarySecurityTokenOutputProcessor();
                initializeOutputProcessor(outputProcessorChain, kerberosTokenOutputProcessor, action, -1);

                final WSSSignatureOutputProcessor signatureOutputProcessor = new WSSSignatureOutputProcessor();
                initializeOutputProcessor(outputProcessorChain, signatureOutputProcessor, action, ++actionOrder);
            } else if (WSSConstants.ENCRYPTION_WITH_KERBEROS_TOKEN.equals(action)) {
                configuredAction.kerberos = true;
                configuredAction.encryptionKerberos = true;
                final BinarySecurityTokenOutputProcessor kerberosTokenOutputProcessor =
                        new BinarySecurityTokenOutputProcessor();
                initializeOutputProcessor(outputProcessorChain, kerberosTokenOutputProcessor, action, -1);

                final EncryptOutputProcessor encryptOutputProcessor = new EncryptOutputProcessor();
                initializeOutputProcessor(outputProcessorChain, encryptOutputProcessor, action, ++actionOrder);
            } else if (WSSConstants.KERBEROS_TOKEN.equals(action)) {
                configuredAction.kerberos = true;
                final BinarySecurityTokenOutputProcessor kerberosTokenOutputProcessor =
                    new BinarySecurityTokenOutputProcessor();
                initializeOutputProcessor(outputProcessorChain, kerberosTokenOutputProcessor, action, -1);
            } else if (WSSConstants.CUSTOM_TOKEN.equals(action)) {
                final CustomTokenOutputProcessor unknownTokenOutputProcessor =
                    new CustomTokenOutputProcessor();
                initializeOutputProcessor(outputProcessorChain, unknownTokenOutputProcessor, action, -1);
            }
        }

        return configuredAction;
    }

    private static final class ConfiguredAction {
        boolean signatureAction = false;
        boolean encryptionAction = false;
        boolean signedSAML = false;
        boolean kerberos = false;
        boolean signatureKerberos = false;
        boolean encryptionKerberos = false;
        boolean derivedSignature = false;
        boolean derivedEncryption = false;
    }
}
