/*
 * JBoss, Home of Professional Open Source.
 * Copyright 2016 Red Hat, Inc., and individual contributors
 * as indicated by the @author tags.
 *
 * Licensed 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.wildfly.security.keystore;

import java.security.KeyStore;
import java.security.KeyStoreSpi;
import java.security.Provider;

import javax.naming.NamingException;
import javax.naming.directory.Attributes;
import javax.naming.directory.DirContext;
import javax.naming.directory.SearchControls;
import javax.naming.ldap.LdapName;

import org.wildfly.common.Assert;
import org.wildfly.common.function.ExceptionSupplier;

/**
 * A LDAP backed {@link KeyStore} implementation.
 *
 * To create the new instances the {@link LdapKeyStore.Builder} should be used.
 *
 * @author <a href="mailto:jkalina@redhat.com">Jan Kalina</a>
 */
public class LdapKeyStore extends KeyStore {

    protected LdapKeyStore(KeyStoreSpi keyStoreSpi, Provider provider, String type) {
        super(keyStoreSpi, provider, type);
    }

    public static Builder builder() {
        return new Builder();
    }

    public static class Builder {

        private static final int DEFAULT_SEARCH_TIME_LIMIT = 10000;

        private ExceptionSupplier<DirContext, NamingException> dirContextSupplier;
        private String searchPath;
        private int searchScope = SearchControls.SUBTREE_SCOPE;
        private int searchTimeLimit = DEFAULT_SEARCH_TIME_LIMIT;

        private String filterAlias;
        private String filterCertificate;
        private String filterIterate;

        private LdapName createPath;
        private String createRdn = "cn";
        private Attributes createAttributes;

        private String aliasAttribute = "cn";
        private String certificateAttribute = "usercertificate";
        private String certificateType = "X.509";
        private String certificateChainAttribute = "userSMIMECertificate";
        private String certificateChainEncoding = "PKCS7";
        private String keyAttribute = "userPKCS12";
        private String keyType = "PKCS12";

        private Builder() {
        }

        /**
         * Build a LDAP keystore.
         *
         * @return the LDAP keystore
         */
        public LdapKeyStore build() {
            Assert.checkNotNullParam("dirContextSupplier", dirContextSupplier);
            Assert.checkNotNullParam("searchPath", searchPath);
            Assert.checkNotNullParam("searchScope", searchScope);
            Assert.checkNotNullParam("searchTimeLimit", searchTimeLimit);

            Assert.checkNotNullParam("aliasAttribute", aliasAttribute);
            Assert.checkNotNullParam("certificateAttribute", certificateAttribute);
            Assert.checkNotNullParam("certificateType", certificateType);
            Assert.checkNotNullParam("certificateChainAttribute", certificateChainAttribute);
            Assert.checkNotNullParam("certificateChainEncoding", certificateChainEncoding);
            Assert.checkNotNullParam("keyAttribute", keyAttribute);
            Assert.checkNotNullParam("keyType", keyType);

            if (filterAlias == null) filterAlias = "(" + aliasAttribute + "={0})";
            if (filterCertificate == null) filterCertificate = "(" + certificateAttribute + "={0})";
            if (filterIterate == null) filterIterate = "(" + aliasAttribute + "=*)";

            LdapKeyStoreSpi spi = new LdapKeyStoreSpi(dirContextSupplier, searchPath, searchScope, searchTimeLimit,
                    filterAlias, filterCertificate, filterIterate, createPath, createRdn, createAttributes, aliasAttribute,
                    certificateAttribute, certificateType, certificateChainAttribute, certificateChainEncoding,
                    keyAttribute, keyType);
            return new LdapKeyStore(spi, EmptyProvider.getInstance(), "LdapKeyStore");
        }

        /**
         * Set the {@link DirContext} supplier, which will be used to obtain DirContext to perform
         * operation over {@link KeyStore}.
         *
         * @param dirContextSupplier
         * @return this builder
         */
        public Builder setDirContextSupplier(ExceptionSupplier<DirContext, NamingException> dirContextSupplier) {
            this.dirContextSupplier = dirContextSupplier;
            return this;
        }

        /**
         * Set the name of the context (DN, distinguish name) to be used when executing queries.
         *
         * @param searchPath the name of the context to search
         * @return this builder
         */
        public Builder setSearchPath(String searchPath) {
            this.searchPath = searchPath;
            return this;
        }

        /**
         * Set if queries are searching the entire subtree (true) or only one level search is used (false).
         * Default value: SUBTREE_SCOPE
         *
         * @return this builder
         */
        public Builder setSearchScope(int searchScope) {
            this.searchScope = searchScope;
            return this;
        }

        /**
         * Set if queries are searching the entire subtree (true) or only one level search is used (false).
         * Default value: true
         *
         * @return this builder
         */
        public Builder setSearchRecursive(boolean recursive) {
            this.searchScope = recursive ? SearchControls.SUBTREE_SCOPE : SearchControls.ONELEVEL_SCOPE;
            return this;
        }

        /**
         * Set the time limit of LDAP search in milliseconds.
         *
         * @param searchTimeLimit the limit in milliseconds. Defaults to {@value #DEFAULT_SEARCH_TIME_LIMIT} milliseconds.
         * @return this builder
         */
        public Builder setSearchTimeLimit(int searchTimeLimit) {
            this.searchTimeLimit = searchTimeLimit;
            return this;
        }

        /**
         * Set the LDAP filter used to search keystore item by alias.
         * If not specified "(alias-attribute={0})" is used.
         *
         * @param filterAlias the LDAP filter, substring "{0}" will by replaced by searched alias
         * @return this builder
         */
        public Builder setFilterAlias(String filterAlias) {
            this.filterAlias = filterAlias;
            return this;
        }

        /**
         * Set the LDAP filter used to search keystore item by certificate.
         * If not specified "(certificate-attribute={0})" is used.
         *
         * @param filterCertificate the LDAP filter, substring "{0}" will by replaced by encoded searched certificate
         * @return this builder
         */
        public Builder setFilterCertificate(String filterCertificate) {
            this.filterCertificate = filterCertificate;
            return this;
        }

        /**
         * Set the LDAP filter used to search all keystore items.
         * If not specified "(alias-attribute=*)" is used.
         *
         * @param filterIterate the LDAP filter
         * @return this builder
         */
        public Builder setFilterIterate(String filterIterate) {
            this.filterIterate = filterIterate;
            return this;
        }

        /**
         * Set the name of the context (DN, distinguish name), where will be LDAP entries of new keystore items created.
         *
         * @param createPath the name of the context, where to create
         * @return this builder
         */
        public Builder setCreatePath(LdapName createPath) {
            this.createPath = createPath;
            return this;
        }

        /**
         * Set the name of the attribute in LDAP, that will be used as RDN - last part of path of new entries.
         * This attribute can be different from aliasAttribute, but its value will be alias too for
         * newly created entries.
         *
         * @param createRdn the name of attribute which will be used as RDN
         * @return this builder
         */
        public Builder setCreateRdn(String createRdn) {
            this.createRdn = createRdn;
            return this;
        }

        /**
         * Set the attributes of newly created LDAP entries and their values.
         *
         * @param createAttributes the attributes and their initial values
         * @return this builder
         */
        public Builder setCreateAttributes(Attributes createAttributes) {
            this.createAttributes = createAttributes;
            return this;
        }

        /**
         * Set the name of the attribute in LDAP that holds the alias of keystore item.
         *
         * @param aliasAttribute the name of attribute that holds the alias
         * @return this builder
         */
        public Builder setAliasAttribute(String aliasAttribute) {
            this.aliasAttribute = aliasAttribute;
            return this;
        }

        /**
         * Set the name of the attribute in LDAP that holds the encoded certificate.
         *
         * @param certificateAttribute the name of attribute that holds the encoded certificate
         * @return this builder
         */
        public Builder setCertificateAttribute(String certificateAttribute) {
            this.certificateAttribute = certificateAttribute;
            return this;
        }

        /**
         * Set the type of certificate, which is stored in certificateAttribute and certificateChainAttribute.
         * This type is used for decoding certificate and certificate chain from LDAP attribute value.
         *
         * @see java.security.cert.CertificateFactory#getInstance(String)
         *
         * @param certificateType the name of attribute that holds the encoded certificate
         * @return this builder
         */
        public Builder setCertificateType(String certificateType) {
            this.certificateType = certificateType;
            return this;
        }

        /**
         * Set the name of the attribute in LDAP that holds the encoded certificate chain.
         *
         * @param certificateChainAttribute the name of attribute that holds the encoded certificate chain
         * @return this builder
         */
        public Builder setCertificateChainAttribute(String certificateChainAttribute) {
            this.certificateChainAttribute = certificateChainAttribute;
            return this;
        }

        /**
         * Set the encoding of certificate chain, which is stored in certificateChainAttribute.
         * This encoding is used for encoding certificate chain into the LDAP attribute value.
         *
         * @see java.security.cert.CertPath#getEncoded(String)
         *
         * @param certificateChainEncoding the name of the encoding to use
         * @return this builder
         */
        public Builder setCertificateChainEncoding(String certificateChainEncoding) {
            this.certificateChainEncoding = certificateChainEncoding;
            return this;
        }

        /**
         * Set the name of the attribute in LDAP that holds the private key.
         * Private key is stored encased in KeyStore, encrypted by password of keystore item.
         *
         * @param keyAttribute the name of attribute that holds the private key
         * @return this builder
         */
        public Builder setKeyAttribute(String keyAttribute) {
            this.keyAttribute = keyAttribute;
            return this;
        }

        /**
         * Set type of keystores, into which is encased every private key before storing into keyAttribute.
         *
         * @see KeyStore#getInstance(String)
         *
         * @param keyType the type of keystore
         * @return this builder
         */
        public Builder setKeyType(String keyType) {
            this.keyType = keyType;
            return this;
        }
    }

}
