class ZTSImpl implements KeyStore, ZTSHandler {

    @Override
    public RoleToken getRoleToken(ResourceContext ctx, String domainName, String roleNames,
            Integer minExpiryTime, Integer maxExpiryTime, String proxyForPrincipal) {

        final String caller = ctx.getApiName();
        final String principalDomain = logPrincipalAndGetDomain(ctx);

        validateRequest(ctx.request(), principalDomain, caller);
        validate(domainName, TYPE_DOMAIN_NAME, principalDomain, caller);
        if (roleNames != null && !roleNames.isEmpty()) {
            validate(roleNames, TYPE_ENTITY_LIST, principalDomain, caller);
        }
        if (proxyForPrincipal != null && !proxyForPrincipal.isEmpty()) {
            validate(proxyForPrincipal, TYPE_ENTITY_NAME, principalDomain, caller);
        }
        
        // for consistent handling of all requests, we're going to convert
        // all incoming object values into lower case since ZMS Server
        // saves all of its object names in lower case
        
        domainName = domainName.toLowerCase();
        setRequestDomain(ctx, domainName);
        if (roleNames != null) {
            roleNames = roleNames.toLowerCase();
        }
        if (proxyForPrincipal != null) {
            proxyForPrincipal = normalizeDomainAliasUser(proxyForPrincipal.toLowerCase());
        }

        // get our principal's name

        final Principal principal = ((RsrcCtxWrapper) ctx).principal();
        String principalName = principal.getFullName();

        if (LOGGER.isDebugEnabled()) {
            LOGGER.debug("getRoleToken(domain: {}, principal: {}, role-name: {}, proxy-for: {})",
                    domainName, principalName, roleNames, proxyForPrincipal);
        }
        
        // do not allow empty (not null) values for role
        
        roleNames = convertEmptyStringToNull(roleNames);
        proxyForPrincipal = convertEmptyStringToNull(proxyForPrincipal);
        
        if (leastPrivilegePrincipal && roleNames == null) {
            throw requestError("getRoleToken: Client must specify a roleName to request a token for",
                    caller, ZTSConsts.ZTS_UNKNOWN_DOMAIN, principalDomain);
        }
        
        // we can only have a proxy for principal request if the original
        // caller is authorized for such operations
        
        if (proxyForPrincipal != null && !isAuthorizedProxyUser(authorizedProxyUsers, principalName)) {
            LOGGER.error("getRoleToken: Principal {} not authorized for proxy role token request", principalName);
            throw forbiddenError("getRoleToken: Principal: " + principalName
                    + " not authorized for proxy role token request", caller,
                    ZTSConsts.ZTS_UNKNOWN_DOMAIN, principalDomain);
        }

        // first retrieve our domain data object from the cache

        DataCache data = dataStore.getDataCache(domainName);
        if (data == null) {
            setRequestDomain(ctx, ZTSConsts.ZTS_UNKNOWN_DOMAIN);
            throw notFoundError("getRoleToken: No such domain: " + domainName, caller,
                    ZTSConsts.ZTS_UNKNOWN_DOMAIN, principalDomain);
        }
        
        // check if the authorized service domain matches to the
        // requested domain name
        
        checkRoleTokenAuthorizedServiceRequest(principal, domainName, caller);
        
        // we need to convert our request role name into array since
        // it could contain multiple values separated by commas
        
        String[] requestedRoleList = null;
        if (roleNames != null) {
            requestedRoleList = roleNames.split(",");
        }
        
        // process our request and retrieve the roles for the principal
        
        Set<String> roles = new HashSet<>();
        dataStore.getAccessibleRoles(data, domainName, principalName, requestedRoleList,
                roles, false);
        
        if (roles.isEmpty()) {
            throw forbiddenError("getRoleToken: No access to any roles in domain: "
                    + domainName, caller, domainName, principalDomain);
        }
        
        // if this is proxy for operation then we want to make sure that
        // both principals have access to the same set of roles so we'll
        // remove any roles that are authorized by only one of the principals
        
        String proxyUser = null;
        if (proxyForPrincipal != null) {
            Set<String> rolesForProxy = new HashSet<>();
            dataStore.getAccessibleRoles(data, domainName, proxyForPrincipal,
                    requestedRoleList, rolesForProxy, false);
            roles.retainAll(rolesForProxy);
            
            // check again in case we removed all the roles and ended up
            // with an empty set
            
            if (roles.isEmpty()) {
                throw forbiddenError("getRoleToken: No access to any roles by User and Proxy Principals",
                        caller, domainName, principalDomain);
            }
            
            // we need to switch our principal and proxy for user
            
            proxyUser = principalName;
            principalName = proxyForPrincipal;
        }

        // if the request was done by a role certificate we need to make sure
        // that it is issued for the roles we're returning in the role token

        if (!isPrincipalRoleCertificateAccessValid(principal, domainName, roles)) {
            throw forbiddenError("getRoleToken: Role based Principal does not include all roles",
                    caller, domainName, principalDomain);
        }

        // generate and return role token

        long tokenTimeout = determineTokenTimeout(data, roles, minExpiryTime, maxExpiryTime);
        List<String> roleList = new ArrayList<>(roles);
        boolean domainCompleteRoleSet = (includeRoleCompleteFlag && roleNames == null);
        com.yahoo.athenz.auth.token.RoleToken token =
                new com.yahoo.athenz.auth.token.RoleToken.Builder(ZTS_ROLE_TOKEN_VERSION, domainName, roleList)
                    .expirationWindow(tokenTimeout).host(serverHostName).keyId(privateKey.getId())
                    .principal(principalName).ip(ServletRequestUtil.getRemoteAddress(ctx.request()))
                    .proxyUser(proxyUser).domainCompleteRoleSet(domainCompleteRoleSet).build();
        token.sign(privateKey.getKey());
        
        RoleToken roleToken = new RoleToken();
        roleToken.setToken(token.getSignedToken());
        roleToken.setExpiryTime(token.getExpiryTime());

        return roleToken;
    }

    public  ZTSImpl();
    public  ZTSImpl(CloudStore implCloudStore, DataStore implDataStore);

}