package act.conf;

/*-
 * #%L
 * ACT Framework
 * %%
 * Copyright (C) 2014 - 2017 ActFramework
 * %%
 * 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.
 * #L%
 */

import static act.conf.AppConfigKey.*;
import static org.osgl.http.H.Header.Names.X_XSRF_TOKEN;

import act.Act;
import act.Constants;
import act.act_messages;
import act.app.*;
import act.app.conf.AppConfigurator;
import act.app.event.AppClassLoaderInitialized;
import act.app.event.SysEventId;
import act.app.util.NamedPort;
import act.cli.CliOverHttpAuthority;
import act.crypto.HMAC;
import act.crypto.RotateSecretHMAC;
import act.data.DateTimeStyle;
import act.data.DateTimeType;
import act.db.util.SequenceNumberGenerator;
import act.db.util._SequenceNumberGenerator;
import act.event.EventBus;
import act.event.SysEventListenerBase;
import act.handler.*;
import act.handler.event.ResultEvent;
import act.i18n.I18n;
import act.internal.util.StrBufRetentionLimitCalculator;
import act.route.Router;
import act.security.CSRFProtector;
import act.session.*;
import act.util.*;
import act.validation.Password;
import act.validation.PasswordSpec;
import act.view.TemplatePathResolver;
import act.view.View;
import act.ws.DefaultSecureTicketCodec;
import act.ws.SecureTicketCodec;
import act.ws.UsernameSecureTicketCodec;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.alibaba.fastjson.parser.ParserConfig;
import me.tongfei.progressbar.ProgressBarStyle;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
import org.osgl.*;
import org.osgl.cache.CacheService;
import org.osgl.cache.CacheServiceProvider;
import org.osgl.exception.ConfigurationException;
import org.osgl.exception.NotAppliedException;
import org.osgl.http.H;
import org.osgl.mvc.MvcConfig;
import org.osgl.util.*;
import org.osgl.util.converter.TypeConverterRegistry;
import org.osgl.web.util.UserAgent;
import org.rythmengine.utils.Time;
import osgl.version.Version;

import java.awt.*;
import java.io.File;
import java.io.IOException;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.net.ServerSocket;
import java.nio.charset.StandardCharsets;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.regex.Pattern;
import javax.inject.Provider;

public class AppConfig<T extends AppConfig> extends Config<AppConfigKey> implements AppHolder<AppConfig<T>> {

    public static final String CONF_FILE_NAME = "app.conf";
    public static final String CSRF_TOKEN_NAME = "__csrf__";
    public static final String PORT_CLI_OVER_HTTP = Router.PORT_CLI_OVER_HTTP;
    public static final String PORT_ADMIN = Router.PORT_ADMIN;
    public static final String PORT_SYS = Router.PORT_SYS;

    private App app;

    static {
        MvcConfig.registerAlarmListener(MvcConfig.ALARM_BIG_CONTENT_ENCOUNTERED, new $.Func0() {
            @Override
            public Object apply() throws NotAppliedException, $.Break {
                ActionContext ctx = ActionContext.current();
                if (null != ctx) {
                    ctx.setLargeResponseHint();
                }
                return null;
            }
        });
        TypeConverterRegistry.INSTANCE.register(new Lang.TypeConverter<String, Class>() {
            @Override
            public Class convert(String s) {
                return Act.appClassForName(s);
            }
        });
        MvcConfig.errorPageRenderer(new ActErrorPageRender());
        MvcConfig.beforeCommitResultHandler(ResultEvent.BEFORE_COMMIT_HANDLER);
        MvcConfig.afterCommitResultHandler(ResultEvent.AFTER_COMMIT_HANDLER);
        MvcConfig.messageTranslater(new $.Transformer<String, String>() {
            @Override
            public String transform(String message) {
                String translated = I18n.i18n(message);
                if (message == translated) {
                    translated = I18n.i18n(MvcConfig.class, message);
                }
                if (message == translated) {
                    translated = I18n.i18n(act_messages.class, message);
                }
                return translated;
            }
        });
        // Refer https://cloud.tencent.com/announce/detail/1112
        ParserConfig.getGlobalInstance().setSafeMode(true);
    }

    private RouterRegexMacroLookup routerRegexMacroLookup;

    /**
     * Construct a <code>AppConfig</code> with a map. The map is copied to
     * the original map of the configuration instance
     *
     * @param configuration
     */
    public AppConfig(Map<String, ?> configuration) {
        super(configuration);
        loadFromConfServer();
        raw.putAll(extendedConfigurations());
    }

    // for unit test
    public AppConfig() {
        this((Map) System.getProperties());
        this.routerRegexMacroLookup = new RouterRegexMacroLookup(this);
    }

    private void loadFromConfServer() {
        String confServerEndpoint = get(CONF_SERVER_ENDPOINT, "");
        if (S.blank(confServerEndpoint)) {
            return;
        }
        E.invalidConfigurationIf(!confServerEndpoint.startsWith("http"), "conf-server.endpoint must be full URL, found: %s", confServerEndpoint);
        String privateKey = confPrivateKey();
        E.invalidConfigurationIf(S.blank(privateKey), "conf.private-key not configured for conf-server: " + confServerEndpoint);
        String confId = confId();
        E.invalidConfigurationIf(S.blank(confId), "conf.id not configured correctly for conf-server: " + confServerEndpoint);
        OkHttpClient http = new OkHttpClient.Builder().build();
        String url = S.concat(confServerEndpoint, "?id=", confId);
        Request req = new Request.Builder().url(url).get().addHeader("Accept", "application/json").build();
        try {
            Response resp = http.newCall(req).execute();
            if (!resp.isSuccessful()) {
                warn("Error fetching configuration from conf-server. Response code: %s; Respond body: %s", resp.code(), resp.body().string());
            } else {
                String data = Crypto.decryptRSA(resp.body().string(), privateKey);
                JSONObject json = JSON.parseObject(data);
                raw.putAll(json);
            }
        } catch (IOException e) {
            warn(e, "Error fetching configuration from conf-server: " + confServerEndpoint);
        }
    }

    public AppConfig<T> app(App app) {
        E.NPE(app);
        this.app = app;
        AppConfigKey.onApp(app);
        EventBus eventBus = app.eventBus();
        if (null != eventBus) {
            eventBus.bind(SysEventId.CLASS_LOADER_INITIALIZED, new SysEventListenerBase<AppClassLoaderInitialized>() {
                @Override
                public void on(AppClassLoaderInitialized event) throws Exception {
                    routerRegexMacroLookup = new RouterRegexMacroLookup(AppConfig.this);
                }
            });
        } // else - must be in routing benchmark unit test
        return this;
    }

    public void preloadConfigurations() {
        long ms = $.ms();
        // ensure JWT get evaluated first to set
        // default value for dependency settings
        jwtEnabled();

        for (Method method : AppConfig.class.getDeclaredMethods()) {
            boolean isPublic = Modifier.isPublic(method.getModifiers());
            if (!isPublic || method.isAnnotationPresent(Lazy.class)) {
                continue;
            }
            if (0 == method.getParameterTypes().length && !"preloadConfigurations".equals(method.getName())) {
                $.invokeVirtual(this, method);
            }
        }

        MvcConfig.renderJsonOutputCharset(renderJsonOutputCharset());
        $.Func0<H.Format> jsonContentProvider = jsonContentTypeProvider();
        if (null != jsonContentTypeProvider) {
            MvcConfig.jsonMediaTypeProvider(jsonContentProvider);
        }

        OsglConfig.setXmlRootTag(xmlRootTag());

        OsglConfig.setThreadLocalBufferLimit(threadLocalBufRetentionLimit());
        OsglConfig.registerGlobalInstanceFactory(new $.Function<Class, Object>() {
            final App app = Act.app();

            @Override
            public Object apply(Class aClass) throws NotAppliedException, $.Break {
                return app.getInstance(aClass);
            }
        });
        OsglConfig.setSingletonChecker(new $.Predicate() {
            @Override
            public boolean test(Object o) {
                return app.isSingleton(o);
            }
        });

        app().cache().setDefaultTTL(cacheTtl());
        Map<String, Object> cacheSettings = subSet("cache");
        for (Map.Entry<String, Object> entry : cacheSettings.entrySet()) {
            String key = entry.getKey();
            if (key.endsWith(".ttl")) {
                String s = S.string(entry.getValue());
                int ttl;
                if (S.isInt(s)) {
                    ttl = Integer.parseInt(s);
                } else if (s.contains("*")) {
                    List<String> sl = S.fastSplit(s, "*");
                    int n = 1;
                    for (String sn : sl) {
                        n *= Integer.parseInt(sn.trim());
                    }
                    ttl = n;
                } else {
                    throw new ConfigurationException("Invalid cache ttl configuration: " + key);
                }
                key = S.cut(key.substring(6)).beforeLast(".");
                app().cache(key).setDefaultTTL(ttl);
            }
        }
        logger.trace("Preload Config takes: %sms", $.ms() - ms);
    }


    public App app() {
        return app;
    }

    public RouterRegexMacroLookup routerRegexMacroLookup() {
        return routerRegexMacroLookup;
    }

    @Override
    protected ConfigKey keyOf(String s) {
        return AppConfigKey.valueOfIgnoreCase(s);
    }

    private Boolean apiDoc;

    protected T enableApiDoc(boolean b) {
        this.apiDoc = b;
        return me();
    }

    public boolean apiDocEnabled() {
        if (null == apiDoc) {
            this.apiDoc = get(API_DOC_EABLED, Act.isDev());
        }
        return this.apiDoc;
    }

    private void _mergeApiDocEnabled(AppConfig conf) {
        if (!hasConfiguration(API_DOC_EABLED)) {
            this.apiDoc = conf.apiDoc;
        }
    }

    private Boolean apiDocBuiltInHide;

    protected T hideBuiltInEndpointsInApiDoc(boolean b) {
        this.apiDocBuiltInHide = b;
        return me();
    }

    public boolean isHideBuiltInEndpointsInApiDoc() {
        if (null == apiDocBuiltInHide) {
            apiDocBuiltInHide = get(API_DOC_HIDE_BUILT_IN_ENDPOINTS, true);
        }
        return apiDocBuiltInHide;
    }

    private void _mergeHideBuiltInEndpointsInApiDoc(AppConfig conf) {
        if (!hasConfiguration(API_DOC_HIDE_BUILT_IN_ENDPOINTS)) {
            this.apiDocBuiltInHide = conf.apiDocBuiltInHide;
        }
    }

    private String appName;

    protected T appName(String name) {
        this.appName = S.requireNotBlank(name);
        return me();
    }

    public String appName() {
        if (S.blank(appName)) {
            // - `app` not initialized yet: appName = get(APP_NAME, app.name());
            appName = get(APP_NAME, Act.app().name());
        }
        return appName;
    }

    private void _mergeAppName(AppConfig conf) {
        if (!hasConfiguration(APP_NAME)) {
            this.appName = conf.appName;
        }
    }

    private Boolean basicAuth;

    protected T enableBasicAuthentication(boolean b) {
        this.basicAuth = b;
        return me();
    }

    public boolean basicAuthenticationEnabled() {
        if (null == basicAuth) {
            this.basicAuth = get(BASIC_AUTHENTICATION, Act.isDev());
        }
        return this.basicAuth;
    }

    private void _mergeBasicAuthentication(AppConfig conf) {
        if (!hasConfiguration(BASIC_AUTHENTICATION)) {
            this.basicAuth = conf.basicAuth;
        }
    }

    private Boolean builtInReqHandlerEnabled;

    protected T disableBuiltInReqHandler() {
        builtInReqHandlerEnabled = false;
        return me();
    }

    public boolean builtInReqHandlerEnabled() {
        if (null == builtInReqHandlerEnabled) {
            builtInReqHandlerEnabled = get(BUILT_IN_REQ_HANDLER_ENABLED, true);
        }
        return builtInReqHandlerEnabled;
    }

    private void _mergeBuiltInReqHandler(AppConfig conf) {
        if (!hasConfiguration(BUILT_IN_REQ_HANDLER_ENABLED)) {
            this.builtInReqHandlerEnabled = conf.builtInReqHandlerEnabled;
        }
    }

    private Boolean cacheForOnDev;

    protected T enableCacheForOnDevMode() {
        cacheForOnDev = false;
        return me();
    }

    public boolean cacheForOnDevMode() {
        if (null == cacheForOnDev) {
            cacheForOnDev = get(CACHE_FOR_ON_DEV, false);
        }
        return cacheForOnDev;
    }

    private void _mergeCacheForOnDev(AppConfig config) {
        if (!hasConfiguration(CACHE_FOR_ON_DEV)) {
            cacheForOnDev = config.cacheForOnDev;
        }
    }

    private Integer captchaWidth;

    protected T captchaWidth(int w) {
        this.captchaWidth = w;
        return me();
    }

    public int captchaWidth() {
        if (null == captchaWidth) {
            captchaWidth = getInteger(CAPTCHA_WIDTH, 200);
        }
        return captchaWidth;
    }

    private void _mergeCaptchaWidth(AppConfig config) {
        if (!hasConfiguration(CAPTCHA_WIDTH)) {
            captchaWidth = config.captchaWidth;
        }
    }

    private Integer captchaHeight;

    protected T captchaHeight(int h) {
        this.captchaHeight = h;
        return me();
    }

    public int captchaHeight() {
        if (null == captchaHeight) {
            captchaHeight = getInteger(CAPTCHA_HEIGHT, 70);
        }
        return captchaHeight;
    }

    private void _mergeCaptchaHeight(AppConfig config) {
        if (!hasConfiguration(CAPTCHA_HEIGHT)) {
            captchaHeight = config.captchaHeight;
        }
    }

    private Color captchaBgColor;

    protected T captchaBgColor(Color color) {
        this.captchaBgColor = color;
        return me();
    }

    public Color captchaBgColor() {
        if (null == captchaBgColor) {
            String s = get(CAPTCHA_BG_COLOR, "white");
            Field f = $.fieldOf(Color.class, s, false);
            if (null == f) {
                warn("Invalid captcha.background.color found: " + s);
                info("will use white as captcha.background.color");
                captchaBgColor = Color.WHITE;
            } else {
                captchaBgColor = $.getFieldValue(null, f);
            }
        }
        return captchaBgColor;
    }

    private void _mergeCaptchaBgColor(AppConfig config) {
        if (!hasConfiguration(CAPTCHA_BG_COLOR)) {
            captchaBgColor = config.captchaBgColor;
        }
    }

    private String reCaptchaSecret;

    protected T reCaptchaSecret(String secret) {
        this.reCaptchaSecret = secret;
        return me();
    }

    public String reCaptchaSecret() {
        if (null != reCaptchaSecret) {
            reCaptchaSecret = get(CAPTCHA_RECAPTCHA_SECRET, "");
        }
        return reCaptchaSecret;
    }

    public boolean reCaptchaActivated() {
        return S.notBlank(reCaptchaSecret());
    }

    private void _mergeReCaptchaSecret(AppConfig config) {
        if (!hasConfiguration(CAPTCHA_RECAPTCHA_SECRET)) {
            reCaptchaSecret = config.reCaptchaSecret;
        }
    }

    private Map<String, Object> extendedConfigurations;

    protected T extendedConfigurations(Map<String, Object> conf) {
        this.extendedConfigurations = conf;
        return me();
    }

    private Map<String, Object> extendedConfigurations() {
        if (null == extendedConfigurations) {
            try {
                Object confLoader = get(CONF_LOADER, new ExtendedAppConfLoader.DumbLoader());
                this.extendedConfigurations = $.invokeVirtual(confLoader, "loadConfigurations");
            } catch (Exception e) {
                warn(e, "Error loading extended configurations");
                this.extendedConfigurations = C.Map();
            }
        }
        return extendedConfigurations;
    }

    private void _mergeExtendedAppConfLoader(AppConfig conf) {
        if (!hasConfiguration(CONF_LOADER)) {
            extendedConfigurations = conf.extendedConfigurations;
        }
    }

    private String confId;

    protected T confId(String key) {
        this.confId = S.requireNotBlank(key);
        return me();
    }

    public String confId() {
        if (S.blank(confId)) {
            confId = get(CONF_ID, S.concat(appName(), "-", Act.profile()));
        }
        return confId;
    }

    private void _mergeConfId(AppConfig conf) {
        if (!hasConfiguration(CONF_ID)) {
            this.confId = conf.confId;
        }
    }

    private String confPrivateKey;

    protected T confPrivateKey(String key) {
        this.confPrivateKey = key;
        return me();
    }
    private String confPrivateKey() {
        if (S.blank(confPrivateKey)) {
            confPrivateKey = get(CONF_PRIVATE_KEY, "");
        }
        return confPrivateKey;
    }
    private void _mergeConfPrivateId(AppConfig conf) {
        if (!hasConfiguration(CONF_PRIVATE_KEY)) {
            confPrivateKey = conf.confPrivateKey;
        }
    }

    private Boolean cors;

    protected T enableCors(boolean b) {
        this.cors = b;
        return me();
    }

    public boolean corsEnabled() {
        if (null == cors) {
            this.cors = get(CORS, false);
        }
        return this.cors;
    }

    private void _mergeCors(AppConfig conf) {
        if (!hasConfiguration(CORS)) {
            this.cors = conf.cors;
        }
    }

    private String corsOrigin;

    protected T corsAllowOrigin(String s) {
        this.corsOrigin = s;
        return me();
    }

    public String corsAllowOrigin() {
        if (null == corsOrigin) {
            corsOrigin = get(CORS_ORIGIN, "*");
        }
        return corsOrigin;
    }

    private void _mergeCorsOrigin(AppConfig conf) {
        if (!hasConfiguration(CORS_ORIGIN)) {
            corsOrigin = conf.corsOrigin;
        }
    }

    private String corsHeaders;

    @Deprecated
    protected T corsHeaders(String s) {
        this.corsHeaders = s;
        return me();
    }

    private String corsHeaders() {
        if (null == corsHeaders) {
            corsHeaders = get(CORS_HEADERS, "");
        }
        return corsHeaders;
    }

    private void _mergeCorsHeaders(AppConfig conf) {
        if (!hasConfiguration(CORS_HEADERS)) {
            corsHeaders = conf.corsHeaders;
        }
    }

    private String corsHeadersExpose;

    protected T corsHeadersExpose(String s) {
        this.corsHeadersExpose = s;
        return me();
    }

    public String corsExposeHeaders() {
        if (null == corsHeadersExpose) {
            corsHeadersExpose = get(CORS_HEADERS_EXPOSE,"");
            if (S.blank(corsHeadersExpose)) {
                corsHeadersExpose = corsHeaders();
                if (S.notBlank(corsHeadersExpose)) {
                    warn("`cors.headers` is deprecated. Please use `cors.headers.expose` instead");
                } else {
                    corsHeadersExpose = "Act-Session-Expires, Authorization, X-XSRF-Token, X-CSRF-Token, Location, Link, Content-Disposition, Content-Length";
                }
            }
        }
        return corsHeadersExpose;
    }

    private void _mergeCorsHeadersExpose(AppConfig conf) {
        if (!hasConfiguration(CORS_HEADERS_EXPOSE)) {
            corsHeadersExpose = conf.corsHeadersExpose;
        }
    }

    private Boolean corsOptionCheck;

    protected T corsOptionCheck(Boolean b) {
        this.corsOptionCheck = b;
        return me();
    }

    public Boolean corsOptionCheck() {
        if (null == corsOptionCheck) {
            corsOptionCheck = get(CORS_CHECK_OPTION_METHOD, true);
        }
        return corsOptionCheck;
    }

    private void _mergeCorsOptionCheck(AppConfig conf) {
        if (!hasConfiguration(CORS_CHECK_OPTION_METHOD)) {
            corsOptionCheck = conf.corsOptionCheck;
        }
    }

    private String corsHeadersAllowed;

    protected T corsAllowHeaders(String s) {
        this.corsHeadersAllowed = s;
        return me();
    }

    public String corsAllowHeaders() {
        if (null == corsHeadersAllowed) {
            corsHeadersAllowed = get(CORS_HEADERS_ALLOWED, "");
            if (S.isBlank(corsHeadersAllowed)) {
                corsHeadersAllowed = corsHeaders();
                if (S.notBlank(corsHeadersAllowed)) {
                    warn("`cors.headers` is deprecated. Please use `cors.headers.allowed` instead");
                } else {
                    corsHeadersAllowed = "X-HTTP-Method-Override, X-Requested-With, Authorization, X-XSRF-Token, X-CSRF-Token";
                }
            }
        }
        return corsHeadersAllowed;
    }

    private void _mergeCorsHeadersAllowed(AppConfig conf) {
        if (!hasConfiguration(CORS_HEADERS_EXPOSE)) {
            corsHeadersAllowed = conf.corsHeadersAllowed;
        }
    }


    private Integer corsMaxAge;

    protected T corsMaxAge(int corsMaxAge) {
        this.corsMaxAge = corsMaxAge;
        return me();
    }

    public int corsMaxAge() {
        if (null == corsMaxAge) {
            corsMaxAge = getInteger(CORS_MAX_AGE, 30 * 60);
        }
        return corsMaxAge;
    }

    private void _mergeCorsMaxAge(AppConfig conf) {
        if (!hasConfiguration(CORS_MAX_AGE)) {
            corsMaxAge = conf.corsMaxAge;
        }
    }

    private Boolean corsAllowCredentials;

    protected T corsAllowCredentials(boolean b) {
        this.corsAllowCredentials = b;
        return me();
    }

    public boolean corsAllowCredentials() {
        if (null == corsAllowCredentials) {
            corsAllowCredentials = get(CORS_ALLOW_CREDENTIALS, false);
        }
        return corsAllowCredentials;
    }

    private void _mergeCorsAllowCredential(AppConfig conf) {
        if (!hasConfiguration(CORS_ALLOW_CREDENTIALS)) {
            corsAllowCredentials = conf.corsAllowCredentials;
        }
    }

    private String contentSecurityPolicy;
    private boolean cspSet;

    protected T contentSecurityPolicy(String policy) {
        this.contentSecurityPolicy = policy;
        cspSet = null != policy;
        return me();
    }

    public String contentSecurityPolicy() {
        if (!cspSet) {
            contentSecurityPolicy = get(CONTENT_SECURITY_POLICY, null);
            cspSet = true;
        }
        return contentSecurityPolicy;
    }

    private void _mergeCsp(AppConfig conf) {
        if (!hasConfiguration(CONTENT_SECURITY_POLICY)) {
            contentSecurityPolicy = conf.contentSecurityPolicy;
            cspSet = null != contentSecurityPolicy;
        }
    }

    private Boolean csrf;

    protected T enableCsrf(boolean b) {
        this.csrf = b;
        return me();
    }

    public boolean csrfEnabled() {
        if (null == csrf) {
            this.csrf = get(CSRF, false);
        }
        return this.csrf;
    }

    private void _mergeCsrf(AppConfig conf) {
        if (!hasConfiguration(CSRF)) {
            this.csrf = conf.csrf;
        }
    }

    private String csrfParamName;

    protected T csrfParamName(String s) {
        this.csrfParamName = s;
        return me();
    }

    public String csrfParamName() {
        if (null == csrfParamName) {
            csrfParamName = get(CSRF_PARAM_NAME, CSRF_TOKEN_NAME);
        }
        return csrfParamName;
    }

    private void _mergeCsrfParamName(AppConfig conf) {
        if (!hasConfiguration(CSRF_PARAM_NAME)) {
            csrfParamName = conf.csrfParamName;
        }
    }

    private CSRFProtector csrfProtector;

    protected T csrfProtector(CSRFProtector protector) {
        this.csrfProtector = $.requireNotNull(protector);
        return me();
    }

    public CSRFProtector csrfProtector() {
        if (null == csrfProtector) {
            try {
                csrfProtector = get(CSRF_PROTECTOR, CSRFProtector.Predefined.HMAC);
            } catch (ConfigurationException e) {
                Object obj = helper.getValFromAliases(raw, CSRF_PROTECTOR.key(), "impl", null);
                if (null != obj) {
                    this.csrfProtector = CSRFProtector.Predefined.valueOfIgnoreCase(obj.toString());
                    if (null != csrfProtector) {
                        set(CSRF_PROTECTOR, csrfProtector);
                        return this.csrfProtector;
                    }
                }
                throw e;
            }
        }
        return csrfProtector;
    }

    private void _mergeCsrfProtector(AppConfig config) {
        if (!hasConfiguration(CSRF_PROTECTOR)) {
            csrfProtector = config.csrfProtector;
        }
    }

    private String csrfCookieName;

    protected T csrfCookieName(String s) {
        this.csrfCookieName = s;
        return me();
    }

    public String csrfCookieName() {
        if (null == csrfCookieName) {
            csrfCookieName = get(CSRF_COOKIE_NAME, "XSRF-TOKEN");
        }
        return csrfCookieName;
    }

    private void _mergeCsrfCookieName(AppConfig conf) {
        if (!hasConfiguration(CSRF_COOKIE_NAME)) {
            csrfCookieName = conf.csrfCookieName;
        }
    }

    private String csrfHeaderName;

    protected T csrfHeaderName(String s) {
        this.csrfHeaderName = s;
        return me();
    }

    public String csrfHeaderName() {
        if (null == csrfHeaderName) {
            csrfHeaderName = get(CSRF_HEADER_NAME, X_XSRF_TOKEN);
        }
        return csrfHeaderName;
    }

    private void _mergeCsrfHeaderName(AppConfig conf) {
        if (!hasConfiguration(CSRF_HEADER_NAME)) {
            csrfHeaderName = conf.csrfHeaderName;
        }
    }

    private Boolean cliEnabled;

    protected T cliEnable(boolean enable) {
        cliEnabled = enable;
        return me();
    }

    public boolean cliEnabled() {
        if (null == cliEnabled) {
            cliEnabled = get(CLI_ENABLED, true);
        }
        return cliEnabled;
    }

    private void _mergeCliEnabled(AppConfig conf) {
        if (!hasConfiguration(CLI_ENABLED)) {
            cliEnabled = conf.cliEnabled;
        }
    }

    private int cliTablePageSz = -1;

    protected T cliTablePageSz(int sz) {
        E.illegalArgumentIf(sz < 1, "CLI table page size not valid: %s", sz);
        this.cliTablePageSz = sz;
        return me();
    }

    public int cliTablePageSize() {
        if (-1 == cliTablePageSz) {
            cliTablePageSz = getInteger(CLI_PAGE_SIZE_TABLE, 22);
        }
        return cliTablePageSz;
    }

    private void _mergeCliTablePageSz(AppConfig conf) {
        if (!hasConfiguration(CLI_PAGE_SIZE_TABLE)) {
            cliTablePageSz = conf.cliTablePageSz;
        }
    }

    private int cliJSONPageSz = -1;

    protected T cliJSONPageSz(int sz) {
        E.illegalArgumentIf(sz < 1, "CLI JSON page size not valid: %s", sz);
        this.cliJSONPageSz = sz;
        return me();
    }

    public int cliJSONPageSize() {
        if (-1 == cliJSONPageSz) {
            cliJSONPageSz = getInteger(CLI_PAGE_SIZE_TABLE, 22);
        }
        return cliJSONPageSz;
    }

    private void _mergeCliJSONPageSz(AppConfig conf) {
        if (!hasConfiguration(CLI_PAGE_SIZE_TABLE)) {
            cliJSONPageSz = conf.cliJSONPageSz;
        }
    }

    private Boolean cliOverHttp;

    protected T cliOverHttp(boolean enabled) {
        this.cliOverHttp = enabled;
        return me();
    }

    public boolean cliOverHttp() {
        if (null == cliOverHttp) {
            cliOverHttp = get(CLI_OVER_HTTP, true);
        }
        return cliOverHttp;
    }

    private void _mergeCliOverHttp(AppConfig config) {
        if (!hasConfiguration(CLI_OVER_HTTP)) {
            cliOverHttp = config.cliOverHttp;
        }
    }

    private ProgressBarStyle cliProgressBarStyle;

    protected T cliProgressBarStyle(ProgressBarStyle style) {
        this.cliProgressBarStyle = style;
        return me();
    }

    public ProgressBarStyle cliProgressBarStyle() {
        if (null == cliProgressBarStyle) {
            String s = get(CLI_PROGRESS_BAR_STYLE, "unicode");
            cliProgressBarStyle = S.eq("ascii", s) ? ProgressBarStyle.ASCII : ProgressBarStyle.UNICODE_BLOCK;
        }
        return cliProgressBarStyle;
    }

    private void _mergeCliProgressBarStyle(AppConfig config) {
        if (!hasConfiguration(CLI_PROGRESS_BAR_STYLE)) {
            cliProgressBarStyle = config.cliProgressBarStyle;
        }
    }

    private CliOverHttpAuthority cliOverHttpAuthority;

    protected T cliOverHttpAuthority(CliOverHttpAuthority authority) {
        this.cliOverHttpAuthority = authority;
        return me();
    }

    public CliOverHttpAuthority cliOverHttpAuthority() {
        if (null == cliOverHttpAuthority) {
            cliOverHttpAuthority = get(CLI_OVER_HTTP_AUTHORITY, new CliOverHttpAuthority.AllowAll());
        }
        return cliOverHttpAuthority;
    }

    private void _mergeCliOverHttpAuthority(AppConfig config) {
        if (!hasConfiguration(CLI_OVER_HTTP_AUTHORITY)) {
            cliOverHttpAuthority = config.cliOverHttpAuthority;
        }
    }

    Integer cliOverHttpPort;

    protected T cliOverHttpPort(int port) {
        this.cliOverHttpPort = port;
        return me();
    }

    public int cliOverHttpPort() {
        if (null == cliOverHttpPort) {
            cliOverHttpPort = get(CLI_OVER_HTTP_PORT, httpPort() + 2);
        }
        return cliOverHttpPort;
    }

    private void _mergeCliOverHttpPort(AppConfig config) {
        if (!hasConfiguration(CLI_OVER_HTTP_PORT)) {
            cliOverHttpPort = config.cliOverHttpPort;
        }
    }

    String cliOverHttpTitle;

    protected T cliOverHttpTitle(String title) {
        this.cliOverHttpTitle = title;
        return me();
    }

    public String cliOverHttpTitle() {
        if (null == cliOverHttpTitle) {
            cliOverHttpTitle = get(CLI_OVER_HTTP_TITLE, "Cli Over Http");
        }
        return cliOverHttpTitle;
    }

    private void _mergeCliOverHttpTitle(AppConfig config) {
        if (!hasConfiguration(CLI_OVER_HTTP_TITLE)) {
            cliOverHttpTitle = config.cliOverHttpTitle;
        }
    }

    Boolean cliOverHttpSysCmd;

    protected T cliOverHttpSysCmd(boolean enabled) {
        this.cliOverHttpSysCmd = enabled;
        return me();
    }

    public boolean cliOverHttpSysCmdEnabled() {
        if (null == cliOverHttpSysCmd) {
            cliOverHttpSysCmd = get(CLI_OVER_HTTP_SYS_CMD, true);
        }
        return cliOverHttpSysCmd;
    }

    private void _mergeCliOverHttpSysCmd(AppConfig config) {
        if (!hasConfiguration(CLI_OVER_HTTP_SYS_CMD)) {
            cliOverHttpSysCmd = config.cliOverHttpSysCmd;
        }
    }

    private Integer cliPort;

    protected T cliPort(int port) {
        E.illegalArgumentIf(port < 1, "port value not valid: %s", port);
        this.cliPort = port;
        return me();
    }

    public int cliPort() {
        if (null == cliPort) {
            cliPort = get(CLI_PORT, httpPort() + 1);
        }
        return cliPort;
    }

    private void _mergeCliPort(AppConfig conf) {
        if (!hasConfiguration(CLI_PORT)) {
            cliPort = conf.cliPort;
        }
    }

    private int cliSessionExpiration = -1;

    protected T cliSessionExpiration(int expire) {
        E.illegalArgumentIf(expire < 1, "cli session expire not valid: %s", expire);
        this.cliSessionExpiration = expire;
        return me();
    }

    public int cliSessionExpiration() {
        if (-1 == cliSessionExpiration) {
            cliSessionExpiration = get(CLI_SESSION_TTL, 300);
        }
        return cliSessionExpiration;
    }

    private void _mergeCliSessionExpiration(AppConfig conf) {
        if (!hasConfiguration(CLI_SESSION_TTL)) {
            cliSessionExpiration = conf.cliSessionExpiration;
        }
    }

    private String dspToken;

    protected T dspToken(final String tokenName) {
        this.dspToken = tokenName;
        return me();
    }

    public String dspToken() {
        if (null == dspToken) {
            dspToken = get(DOUBLE_SUBMISSION_PROTECT_TOKEN, "act_dsp_token");
        }
        return dspToken;
    }

    private void _mergeDspToken(AppConfig conf) {
        if (!hasConfiguration(DOUBLE_SUBMISSION_PROTECT_TOKEN)) {
            dspToken = conf.dspToken;
        }
    }

    private Provider<String> cookieDomainProvider;

    protected T cookieDomain(final String domain) {
        this.cookieDomainProvider = new Provider<String>() {
            @Override
            public String get() {
                return domain;
            }
        };
        return me();
    }

    protected T cookieDomainProvider(Provider<String> provider) {
        this.cookieDomainProvider = $.requireNotNull(provider);
        return me();
    }

    public String cookieDomain() {
        String domain = cookieDomainProvider().get();
        return "localhost".equals(domain) ? null : domain;
    }

    private Provider<String> cookieDomainProvider() {
        if (null == cookieDomainProvider) {
            try {
                cookieDomainProvider = get(COOKIE_DOMAIN_PROVIDER, new Provider<String>() {
                    @Override
                    public String get() {
                        return host();
                    }
                });
            } catch (ConfigurationException e) {
                Object obj = helper.getValFromAliases(raw, COOKIE_DOMAIN_PROVIDER.key(), "impl", null);
                String s = obj.toString();
                if ("dynamic".equalsIgnoreCase(s) || "flexible".equalsIgnoreCase(s) || "contextual".equalsIgnoreCase(s)) {
                    cookieDomainProvider = new Provider<String>() {
                        @Override
                        public String get() {
                            H.Request req = ActionContext.current().req();
                            return req.domain();
                        }
                    };
                    return cookieDomainProvider;
                }
                throw e;
            }
        }
        return cookieDomainProvider;
    }

    private void _mergeCookieDomain(AppConfig config) {
        if (!hasConfiguration(COOKIE_DOMAIN_PROVIDER)) {
            cookieDomainProvider = config.cookieDomainProvider;
        }
    }

    private int maxCliSession = -1;

    protected T maxCliSession(int size) {
        E.illegalArgumentIf(size < 1, "max cli session number cannot be zero or negative number: %s", size);
        this.maxCliSession = size;
        return me();
    }

    public int maxCliSession() {
        if (-1 == maxCliSession) {
            maxCliSession = get(CLI_SESSION_MAX, 3);
        }
        return maxCliSession;
    }

    private void _mergeMaxCliSession(AppConfig conf) {
        if (!hasConfiguration(CLI_SESSION_MAX)) {
            maxCliSession = conf.maxCliSession;
        }
    }

    private Boolean enumResolvingCaseSensitive;

    protected T enumResolvingCaseSensitive(boolean b) {
        enumResolvingCaseSensitive = b;
        return me();
    }

    @Deprecated
    public boolean enumResolvingCaseSensitive() {
        synchronized (ENUM_RESOLVING_CASE_SENSITIVE) {
            if (null == enumResolvingCaseSensitive) {
                enumResolvingCaseSensitive = get(ENUM_RESOLVING_CASE_SENSITIVE, false);
            }
            return enumResolvingCaseSensitive;
        }
    }

    private void _mergeEnumResolvingCaseSensitive(AppConfig conf) {
        if (!hasConfiguration(ENUM_RESOLVING_CASE_SENSITIVE)) {
            enumResolvingCaseSensitive = conf.enumResolvingCaseSensitive;
        }
    }

    private Boolean enumResolvingExactMatch;

    protected T enumResolvingExactMatch(boolean b) {
        enumResolvingExactMatch = b;
        return me();
    }

    public boolean enumResolvingExactMatch() {
        synchronized (ENUM_RESOLVING_EXACT_MATCH) {
            if (null == enumResolvingExactMatch) {
                enumResolvingExactMatch = get(ENUM_RESOLVING_EXACT_MATCH, enumResolvingCaseSensitive());
            }
            return enumResolvingExactMatch;
        }
    }

    private void _mergeEnumResolvingExactMatch(AppConfig config) {
        if (!hasConfiguration(ENUM_RESOLVING_EXACT_MATCH)) {
            enumResolvingExactMatch = config.enumResolvingExactMatch;
        }
    }

    private String defViewName = null;
    private View defView = null;

    protected T defaultView(View view) {
        E.NPE(view);
        defView = view;
        return me();
    }

    public View defaultView() {
        if (null == defViewName) {
            defViewName = get(AppConfigKey.VIEW_DEFAULT, "rythm");
            defView = Act.viewManager().view(defViewName);
        }
        return defView;
    }

    private void _mergeDefaultView(AppConfig conf) {
        if (!hasConfiguration(AppConfigKey.VIEW_DEFAULT)) {
            defViewName = conf.defViewName;
            defView = conf.defView;
        }
    }

    private String xForwardedProtocol = null;

    protected T forceHttps() {
        xForwardedProtocol = "https";
        return me();
    }

    public String xForwardedProtocol() {
        if (null == xForwardedProtocol) {
            xForwardedProtocol = get(X_FORWARD_PROTOCOL, "http");
        }
        return xForwardedProtocol;
    }

    private void _mergeXForwardedProtocol(AppConfig conf) {
        if (!hasConfiguration(X_FORWARD_PROTOCOL)) {
            xForwardedProtocol = conf.xForwardedProtocol;
        }
    }

    private String xmlRootTag;
    protected T xmlRootTag(String tag) {
        this.xmlRootTag = tag;
        return me();
    }
    public String xmlRootTag() {
        if (null == xmlRootTag) {
            xmlRootTag = get(XML_ROOT, "xml");
        }
        return xmlRootTag;
    }
    private void _mergeXmlRootTag(AppConfig conf) {
        if (!hasConfiguration(XML_ROOT)) {
            this.xmlRootTag = conf.xmlRootTag;
        }
    }

    private ReturnValueAdvice globalReturnValueAdvice;
    private Boolean globalReturnValueAdviceSet;

    protected T globalReturnValueAdvice(ReturnValueAdvice advice) {
        this.globalReturnValueAdvice = $.requireNotNull(advice);
        this.globalReturnValueAdviceSet = true;
        return me();
    }

    public ReturnValueAdvice globalReturnValueAdvice() {
        if (null != globalReturnValueAdviceSet) {
            return globalReturnValueAdvice;
        }
        String s = get(GLOBAL_RETURN_VALUE_ADVICE, null);
        if (null != s) {
            try {
                globalReturnValueAdvice = app.getInstance(s);
            } catch (Exception e) {
                throw new ConfigurationException("Error loading global returnValueAdvice: " + s);
            }
        }
        globalReturnValueAdviceSet = true;
        return globalReturnValueAdvice;
    }

    private void _mergeGlobalReturnValueAdvice(AppConfig conf) {
        if (!hasConfiguration(GLOBAL_RETURN_VALUE_ADVICE)) {
            globalReturnValueAdvice = conf.globalReturnValueAdvice;
            globalReturnValueAdviceSet = conf.globalReturnValueAdviceSet;
        }
    }

    private ValidateViolationAdvice globalValidateViolationAdvice;
    private Boolean globalValidateViolationAdviceSet;

    protected T globalValidateViolationAdvice(ValidateViolationAdvice advice) {
        this.globalValidateViolationAdvice = $.requireNotNull(advice);
        this.globalValidateViolationAdviceSet = true;
        return me();
    }

    public ValidateViolationAdvice globalValidateViolationAdvice() {
        if (null != globalValidateViolationAdviceSet) {
            return globalValidateViolationAdvice;
        }
        String s = get(GLOBAL_VALIDATE_VIOLATION_ADVICE, null);
        if (null != s) {
            try {
                globalValidateViolationAdvice = app.getInstance(s);
            } catch (Exception e) {
                throw new ConfigurationException("Error loading global returnValueAdvice: " + s);
            }
        }
        globalValidateViolationAdviceSet = true;
        return globalValidateViolationAdvice;
    }

    private void _mergeGlobalValidateViolationAdvice(AppConfig conf) {
        if (!hasConfiguration(GLOBAL_VALIDATE_VIOLATION_ADVICE)) {
            globalValidateViolationAdvice = conf.globalValidateViolationAdvice;
            globalValidateViolationAdviceSet = conf.globalValidateViolationAdviceSet;
        }
    }


    private Boolean contentSuffixAware = null;

    protected T contentSuffixAware(boolean b) {
        contentSuffixAware = b;
        return me();
    }

    public Boolean contentSuffixAware() {
        if (null == contentSuffixAware) {
            contentSuffixAware = get(CONTENT_SUFFIX_AWARE, false);
        }
        return contentSuffixAware;
    }

    private void _mergeContentSuffixAware(AppConfig conf) {
        if (!hasConfiguration(CONTENT_SUFFIX_AWARE)) {
            contentSuffixAware = conf.contentSuffixAware;
        }
    }

    private _SequenceNumberGenerator seqGen = null;

    protected T sequenceNumberGenerator(_SequenceNumberGenerator seqGen) {
        this.seqGen = seqGen;
        return me();
    }

    public _SequenceNumberGenerator sequenceNumberGenerator() {
        if (null == seqGen) {
            javax.inject.Provider<_SequenceNumberGenerator> provider = app().getInstance(SequenceNumberGenerator.Provider.class);
            seqGen = get(DB_SEQ_GENERATOR, provider.get());
        }
        return seqGen;
    }

    private void _mergeSequenceNumberGenerator(AppConfig conf) {
        if (!hasConfiguration(DB_SEQ_GENERATOR)) {
            seqGen = conf.seqGen;
        }
    }

    private ErrorTemplatePathResolver errorTemplatePathResolver = null;

    protected T errorTemplatePathResolver(ErrorTemplatePathResolver resolver) {
        errorTemplatePathResolver = resolver;
        return me();
    }

    public ErrorTemplatePathResolver errorTemplatePathResolver() {
        if (null == errorTemplatePathResolver) {
            errorTemplatePathResolver = get(RESOLVER_ERROR_TEMPLATE_PATH, new ErrorTemplatePathResolver.DefaultErrorTemplatePathResolver());
        }
        return errorTemplatePathResolver;
    }

    private void _mergeErrorTemplatePathResolver(AppConfig conf) {
        if (!hasConfiguration(RESOLVER_ERROR_TEMPLATE_PATH)) {
            errorTemplatePathResolver = conf.errorTemplatePathResolver;
        }
    }

    private Boolean headerOverwrite;

    protected T allowHeaderOverwrite(boolean b) {
        headerOverwrite = b;
        return me();
    }

    public boolean allowHeaderOverwrite() {
        if (null == headerOverwrite) {
            headerOverwrite = get(HEADER_OVERWRITE, false);
        }
        return headerOverwrite;
    }

    private void _mergeHeaderOverwrite(AppConfig config) {
        if (!hasConfiguration(HEADER_OVERWRITE)) {
            headerOverwrite = config.headerOverwrite;
        }
    }


    private String headerSessionExpiration;

    protected T headerSessionExpiration(String headerName) {
        headerSessionExpiration = headerName.trim();
        return me();
    }

    public String headerSessionExpiration() {
        if (null == headerSessionExpiration) {
            headerSessionExpiration = get(AppConfigKey.HEADER_SESSION_EXPIRATION, "Act-Session-Expires");
        }
        return headerSessionExpiration;
    }

    private void _mergeHeaderSessionExpiration(AppConfig config) {
        if (!hasConfiguration(HEADER_SESSION_EXPIRATION)) {
            headerSessionExpiration = config.headerSessionExpiration;
        }
    }

    private String host = null;

    protected T host(String hostname) {
        hostname = hostname.trim();
        E.illegalArgumentIf(hostname.length() == 0, "hostname cannot be empty");
        host = hostname;
        return me();
    }

    public String host() {
        if (null == host) {
            host = get(HOST, "localhost");
        }
        return host;
    }

    private void _mergeHost(AppConfig conf) {
        if (!hasConfiguration(HOST)) {
            host = conf.host;
        }
    }

    private Boolean i18nEnabled;

    protected T i18n(boolean enabled) {
        i18nEnabled = enabled;
        return me();
    }

    public boolean i18nEnabled() {
        if (null == i18nEnabled) {
            i18nEnabled = get(I18N, false);
        }
        return i18nEnabled;
    }

    private void _mergeI18nEnabled(AppConfig conf) {
        if (!hasConfiguration(I18N)) {
            i18nEnabled = conf.i18nEnabled;
        }
    }

    private Boolean jwtEnabled;

    protected T jwtEnabled(boolean enabled) {
        jwtEnabled = enabled;
        return me();
    }

    public boolean jwtEnabled() {
        if (null == jwtEnabled) {
            jwtEnabled = get(JWT, false);
            if (jwtEnabled) {
                if (!hasConfiguration(SESSION_HEADER)) {
                    sessionHeader("Authorization");
                }
                if (!hasConfiguration(SESSION_HEADER_PAYLOAD_PREFIX)) {
                    sessionHeaderPayloadPrefix("Bearer ");
                }
                if (!hasConfiguration(SESSION_MAPPER)) {
                    sessionMapper(new HeaderTokenSessionMapper(this));
                }
                if (!hasConfiguration(SESSION_CODEC)) {
                    sessionCodec(app().getInstance(JsonWebTokenSessionCodec.class));
                }
            }
        }
        return jwtEnabled;
    }

    private void _mergeJWT(AppConfig config) {
        if (!hasConfiguration(JWT)) {
            jwtEnabled = config.jwtEnabled;
        }
    }

    private HMAC jwtAlgo;

    protected T jwtArgo(HMAC.Algorithm algo) {
        jwtAlgo = new HMAC(secret(), algo);
        return me();
    }

    public HMAC jwtAlgo() {
        if (null == jwtAlgo) {
            String algoKey = get(JWT_ALGO, "SHA256");
            if (rotateSecret()) {
                jwtAlgo = new RotateSecretHMAC(algoKey, app.getInstance(RotationSecretProvider.class));
            } else {
                jwtAlgo = new HMAC(secret(), algoKey);
            }
        }
        return jwtAlgo;
    }

    private void _mergeJWTAlgo(AppConfig config) {
        if (!hasConfiguration(JWT_ALGO)) {
            jwtAlgo = config.jwtAlgo;
        }
    }

    private String jwtIssuer;

    protected T jwtIssuer(String issuer) {
        E.illegalArgumentIf(S.blank(issuer), "issuer cannot be empty");
        jwtIssuer = issuer;
        return me();
    }

    public String jwtIssuer() {
        if (null == jwtIssuer) {
            jwtIssuer = get(AppConfigKey.JWT_ISSUER, cookiePrefix().substring(0, cookiePrefix().length() - 1));
        }
        return jwtIssuer;
    }

    private void _mergeJwtIssuer(AppConfig config) {
        if (!hasConfiguration(JWT_ISSUER)) {
            jwtIssuer = config.jwtIssuer;
        }
    }

    private String localeParamName;

    protected T localeParamName(String name) {
        E.illegalArgumentIf(S.blank(name), "locale param name must not be empty");
        localeParamName = name;
        return me();
    }

    public String localeParamName() {
        if (null == localeParamName) {
            localeParamName = get(I18N_LOCALE_PARAM_NAME, "act_locale");
        }
        return localeParamName;
    }

    private void _mergeLocaleParamName(AppConfig conf) {
        if (!hasConfiguration(I18N_LOCALE_PARAM_NAME)) {
            localeParamName = conf.localeParamName;
        }
    }


    private String localeCookieName;

    protected T localeCookieName(String name) {
        E.illegalArgumentIf(S.blank(name), "locale Cookie name must not be empty");
        localeCookieName = name;
        return me();
    }

    public String localeCookieName() {
        if (null == localeCookieName) {
            localeCookieName = get(I18N_LOCALE_COOKIE_NAME, "locale");
        }
        return localeCookieName;
    }

    private void _mergeLocaleCookieName(AppConfig conf) {
        if (!hasConfiguration(I18N_LOCALE_COOKIE_NAME)) {
            localeCookieName = conf.localeCookieName;
        }
    }

    private Boolean mockServer;
    protected T mockServer(boolean enabled) {
        mockServer = enabled;
        return me();
    }
    public boolean mockServer() {
        if (null == mockServer) {
            mockServer = get(MOCK_SERVER_ENABLED, app.isDev());
        }
        return mockServer;
    }
    private void _mergeMockServer(AppConfig config) {
        if (!hasConfiguration(MOCK_SERVER_ENABLED)) {
            mockServer = config.mockServer;
        }
    }

    Integer ipEffectiveBytes;

    protected T ipEffectiveBytes(int n) {
        E.illegalArgumentIf(n < 1 || n > 4, "integer from 1 to 4 (inclusive) expected");
        ipEffectiveBytes = n;
        return me();
    }

    public int ipEffectiveBytes() {
        if (null == ipEffectiveBytes) {
            ipEffectiveBytes = getInteger(ID_GEN_NODE_ID_EFFECTIVE_IP_BYTES, 4);
        }
        return ipEffectiveBytes;
    }

    private void _mergeIpEffectiveBytes(AppConfig conf) {
        if (!hasConfiguration(ID_GEN_NODE_ID_EFFECTIVE_IP_BYTES)) {
            ipEffectiveBytes = conf.ipEffectiveBytes;
        }
    }

    private IdGenerator.NodeIdProvider nodeIdProvider;

    protected T nodeIdProvider(IdGenerator.NodeIdProvider provider) {
        this.nodeIdProvider = $.NPE(provider);
        return me();
    }

    public IdGenerator.NodeIdProvider nodeIdProvider() {
        if (null == nodeIdProvider) {
            nodeIdProvider = get(ID_GEN_NODE_ID_PROVIDER, new IdGenerator.NodeIdProvider.IpProvider(ipEffectiveBytes()));
        }
        return nodeIdProvider;
    }

    private void _mergeNodeIdProvider(AppConfig conf) {
        if (!hasConfiguration(ID_GEN_NODE_ID_PROVIDER)) {
            nodeIdProvider = conf.nodeIdProvider;
        }
    }

    private String startIdFile;

    protected T startIdFile(String file) {
        E.illegalArgumentIf(S.blank(file));
        startIdFile = file;
        return me();
    }

    public String startIdFile() {
        if (null == startIdFile) {
            startIdFile = get(ID_GEN_START_ID_FILE, ".act.id-app");
        }
        return startIdFile;
    }

    private void _mergeStartIdFile(AppConfig conf) {
        if (!hasConfiguration(ID_GEN_START_ID_FILE)) {
            startIdFile = conf.startIdFile;
        }
    }

    private IdGenerator.StartIdProvider startIdProvider;

    protected T startIdProvider(IdGenerator.StartIdProvider provider) {
        startIdProvider = $.NPE(provider);
        return me();
    }

    public IdGenerator.StartIdProvider startIdProvider() {
        if (null == startIdProvider) {
            startIdProvider = get(ID_GEN_START_ID_PROVIDER, new IdGenerator.StartIdProvider.DefaultStartIdProvider(startIdFile()));
        }
        return startIdProvider;
    }

    private void _mergeStartIdProvider(AppConfig conf) {
        if (!hasConfiguration(ID_GEN_START_ID_PROVIDER)) {
            startIdProvider = conf.startIdProvider;
        }
    }

    private IdGenerator.SequenceProvider sequenceProvider;

    protected T sequenceProvider(IdGenerator.SequenceProvider provider) {
        this.sequenceProvider = $.NPE(provider);
        return me();
    }

    public IdGenerator.SequenceProvider sequenceProvider() {
        if (null == sequenceProvider) {
            sequenceProvider = get(ID_GEN_SEQ_ID_PROVIDER, new IdGenerator.SequenceProvider.AtomicLongSeq());
        }
        return sequenceProvider;
    }

    private void _mergeSequenceProvider(AppConfig conf) {
        if (!hasConfiguration(ID_GEN_SEQ_ID_PROVIDER)) {
            sequenceProvider = conf.sequenceProvider;
        }
    }


    private IdGenerator.LongEncoder longEncoder;

    protected T longEncoder(IdGenerator.LongEncoder longEncoder) {
        this.longEncoder = $.NPE(longEncoder);
        return me();
    }

    public IdGenerator.LongEncoder longEncoder() {
        if (null == longEncoder) {
            longEncoder = get(ID_GEN_LONG_ENCODER, IdGenerator.SAFE_ENCODER);
        }
        return longEncoder;
    }

    private void _mergeLongEncoder(AppConfig conf) {
        if (!hasConfiguration(ID_GEN_LONG_ENCODER)) {
            longEncoder = conf.longEncoder;
        }
    }

    private String loginUrl = null;

    protected T loginUrl(String url) {
        E.illegalArgumentIf(!url.startsWith("/"), "login URL shall start with '/'");
        this.loginUrl = url;
        return me();
    }

    public String loginUrl() {
        ActionContext context = ActionContext.current();
        if (null != context && context.isAjax()) {
            return ajaxLoginUrl();
        }
        return loginUrl0();
    }

    private String loginUrl0() {
        if (null == loginUrl) {
            loginUrl = get(URL_LOGIN, "/login");
        }
        return loginUrl;
    }

    private void _mergeLoginUrl(AppConfig conf) {
        if (!hasConfiguration(URL_LOGIN)) {
            loginUrl = conf.loginUrl;
        }
    }

    private String ajaxLoginUrl = null;

    protected T ajaxLoginUrl(String url) {
        E.illegalArgumentIf(!url.startsWith("/"), "login URL shall start with '/'");
        this.ajaxLoginUrl = url;
        return me();
    }

    public String ajaxLoginUrl() {
        if (null == ajaxLoginUrl) {
            ajaxLoginUrl = get(URL_LOGIN_AJAX, loginUrl0());
        }
        return ajaxLoginUrl;
    }

    private void _mergeAjaxLoginUrl(AppConfig conf) {
        if (!hasConfiguration(URL_LOGIN_AJAX)) {
            ajaxLoginUrl = conf.ajaxLoginUrl;
        }
    }

    private boolean urlContextInitialized;
    private String urlContext;

    protected T urlContext(String context) {
        this.urlContext = context;
        urlContextInitialized = S.notBlank(context);
        return me();
    }

    public String urlContext() {
        if (!urlContextInitialized) {
            urlContext = get(URL_CONTEXT, null);
            if (null != urlContext) {
                urlContext = urlContext.trim();
                if (urlContext.length() == 0) {
                    urlContext = null;
                } else {
                    while (urlContext.endsWith("/")) {
                        urlContext = urlContext.substring(0, urlContext.length() - 1);
                    }
                    if (urlContext.length() == 0) {
                        urlContext = null;
                    } else if (!urlContext.startsWith("/")) {
                        urlContext = S.concat("/", urlContext);
                    }
                }
                if (null != urlContext && urlContext.contains(" ")) {
                    throw E.invalidConfiguration("url context shall not contains white space");
                }
            }
            urlContextInitialized = true;
        }
        return urlContext;
    }

    private void _mergeUrlContext(AppConfig conf) {
        if (!hasConfiguration(URL_CONTEXT)) {
            urlContext = conf.urlContext;
            urlContextInitialized = conf.urlContextInitialized;
        }
    }


    private int httpMaxParams = -1;

    protected T httpMaxParams(int max) {
        E.illegalArgumentIf(max < 0, "max params cannot be negative number: %s", max);
        this.httpMaxParams = max;
        return me();
    }

    public int httpMaxParams() {
        if (-1 == httpMaxParams) {
            httpMaxParams = getInteger(HTTP_MAX_PARAMS, 128);
            if (httpMaxParams < 0) {
                throw new ConfigurationException("http.params.max setting cannot be negative number. Found: %s", httpMaxParams);
            }
        }
        return httpMaxParams;
    }

    private void _mergeHttpMaxParams(AppConfig conf) {
        if (!hasConfiguration(HTTP_MAX_PARAMS)) {
            httpMaxParams = conf.httpMaxParams;
        }
    }

    private int jobPoolSize = -1;

    protected T jobPoolSize(int size) {
        E.illegalArgumentIf(size < 1, "job pool size cannot be zero or negative number: %s", size);
        this.jobPoolSize = size;
        return me();
    }

    public int jobPoolSize() {
        if (-1 == jobPoolSize) {
            jobPoolSize = getInteger(JOB_POOL_SIZE, 10);
        }
        return jobPoolSize;
    }

    private void _mergeJobPoolSize(AppConfig conf) {
        if (!hasConfiguration(JOB_POOL_SIZE)) {
            jobPoolSize = conf.jobPoolSize;
        }
    }

    private Boolean jsonBodyPatch;
    protected T jsonBodyPatch(boolean enabled) {
        jsonBodyPatch = enabled;
        return me();
    }
    public boolean allowJsonBodyPatch() {
        if (null == jsonBodyPatch) {
            jsonBodyPatch = get(JSON_BODY_PATCH, true);
        }
        return jsonBodyPatch;
    }
    private void _mergeJsonBodyPatch(AppConfig conf) {
        if (!hasConfiguration(JSON_BODY_PATCH)) {
            jsonBodyPatch = conf.jsonBodyPatch;
        }
    }

    private int httpExternalPort = -1;

    protected T httpExternalPort(int port) {
        E.illegalArgumentIf(port < 1, "port value not valid: %s", port);
        this.httpExternalPort = port;
        return me();
    }

    public int httpExternalPort() {
        if (-1 == httpExternalPort) {
            httpExternalPort = getInteger(HTTP_EXTERNAL_PORT, httpExternal() ? 80 : httpPort());
        }
        return httpExternalPort;
    }

    private void _mergeHttpExternalPort(AppConfig conf) {
        if (!hasConfiguration(HTTP_EXTERNAL_PORT)) {
            this.httpExternalPort = conf.httpExternalPort();
        }
    }

    private Boolean httpExternal = null;

    protected T httpExternal(boolean setting) {
        this.httpExternal = setting;
        return me();
    }

    public boolean httpExternal() {
        if (null == httpExternal) {
            httpExternal = get(HTTP_EXTERNAL_SERVER, Act.isProd());
        }
        return httpExternal;
    }

    private void _mergeHttpExternal(AppConfig conf) {
        if (!hasConfiguration(HTTP_EXTERNAL_SERVER)) {
            httpExternal = conf.httpExternal;
        }
    }

    private int httpExternalSecurePort = -1;

    protected T httpExternalSecurePort(int port) {
        E.illegalArgumentIf(port < 1, "port value not valid: %s", port);
        this.httpExternalSecurePort = port;
        return me();
    }

    public int httpExternalSecurePort() {
        if (-1 == httpExternalSecurePort) {
            httpExternalSecurePort = get(HTTP_EXTERNAL_SECURE_PORT, httpExternal() ? 443 : httpPort());
        }
        return httpExternalSecurePort;
    }

    private void _mergeHttpExternalSecurePort(AppConfig conf) {
        if (!hasConfiguration(HTTP_EXTERNAL_PORT)) {
            this.httpExternalSecurePort = conf.httpExternalSecurePort;
        }
    }

    private int httpPort = -1;

    protected T httpPort(int port) {
        E.illegalArgumentIf(port < 1, "port value not valid: %s", port);
        this.httpPort = port;
        return me();
    }

    public int httpPort() {
        if (-1 == httpPort) {
            if ("test".equalsIgnoreCase(Act.profile())) {
                httpPort = randomPort();
            } else {
                httpPort = get(HTTP_PORT, 5460);
            }
        }
        return httpPort;
    }

    private void _mergeHttpPort(AppConfig conf) {
        if (!hasConfiguration(HTTP_PORT)) {
            httpPort = conf.httpPort;
        }
    }

    private static int randomPort() {
        ServerSocket ss = null;
        try {
            ss = new ServerSocket(0);
            return ss.getLocalPort();
        } catch (IOException e) {
            throw E.ioException(e);
        } finally {
            IO.close(ss);
        }
    }

    private Boolean httpSecure = null;

    protected T httpSecure(boolean secure) {
        this.httpSecure = secure;
        return me();
    }

    public boolean httpSecure() {
        if (null == httpSecure) {
            httpSecure = get(HTTP_SECURE, !Act.isDev() && S.neq(Act.profile(), "dev", S.IGNORECASE));
        }
        return httpSecure;
    }

    private void _mergeHttpSecure(AppConfig conf) {
        if (!hasConfiguration(HTTP_SECURE)) {
            httpSecure = conf.httpSecure;
        }
    }


    private int httpsPort = -1;

    protected T httpsPort(int port) {
        E.illegalArgumentIf(port < 1, "port value not valid: %s", port);
        this.httpsPort = port;
        return me();
    }

    public int httpsPort() {
        if (-1 == httpsPort) {
            httpsPort = get(HTTPS_PORT, 5443);
        }
        return httpsPort;
    }

    private void _mergeHttpsPort(AppConfig conf) {
        if (!hasConfiguration(HTTPS_PORT)) {
            httpsPort = conf.httpsPort;
        }
    }

    private MissingAuthenticationHandler mah = null;

    protected T missingAuthenticationHandler(MissingAuthenticationHandler handler) {
        E.NPE(handler);
        mah = handler;
        return me();
    }

    public MissingAuthenticationHandler missingAuthenticationHandler() {
        if (null == mah) {
            RedirectToLoginUrl redirectToLoginUrl = app.getInstance(RedirectToLoginUrl.class);
            MissingAuthenticationHandler defHandler = redirectToLoginUrl.hasLoginUrl() ? redirectToLoginUrl : app.getInstance(ReturnUnauthorized.class);
            mah = get(HANDLER_MISSING_AUTHENTICATION, defHandler);
        }
        return mah;
    }

    private void _mergeMissingAuthenticationHandler(AppConfig config) {
        if (!hasConfiguration(HANDLER_MISSING_AUTHENTICATION)) {
            mah = config.mah;
        }
    }

    private MissingAuthenticationHandler ajaxMah = null;

    protected T ajaxMissingAuthenticationHandler(MissingAuthenticationHandler handler) {
        E.NPE(handler);
        ajaxMah = handler;
        return me();
    }

    public MissingAuthenticationHandler ajaxMissingAuthenticationHandler() {
        if (null == ajaxMah) {
            ajaxMah = get(HANDLER_MISSING_AUTHENTICATION_AJAX, missingAuthenticationHandler());
        }
        return ajaxMah;
    }

    private void _mergeAjaxMissingAuthenticationHandler(AppConfig config) {
        if (!hasConfiguration(HANDLER_MISSING_AUTHENTICATION_AJAX)) {
            ajaxMah = config.ajaxMah;
        }
    }

    private MissingAuthenticationHandler csrfCheckFailureHandler = null;

    protected T csrfCheckFailureHandler(MissingAuthenticationHandler handler) {
        E.NPE(handler);
        csrfCheckFailureHandler = handler;
        return me();
    }

    public MissingAuthenticationHandler csrfCheckFailureHandler() {
        if (null == csrfCheckFailureHandler) {
            csrfCheckFailureHandler = get(HANDLER_CSRF_CHECK_FAILURE, missingAuthenticationHandler());
        }
        return csrfCheckFailureHandler;
    }

    private void _mergeCsrfCheckFailureHandler(AppConfig config) {
        if (!hasConfiguration(HANDLER_CSRF_CHECK_FAILURE)) {
            csrfCheckFailureHandler = config.csrfCheckFailureHandler;
        }
    }

    private MissingAuthenticationHandler ajaxCsrfCheckFailureHandler = null;

    protected T ajaxCsrfCheckFailureHandler(MissingAuthenticationHandler handler) {
        E.NPE(handler);
        ajaxCsrfCheckFailureHandler = handler;
        return me();
    }

    public MissingAuthenticationHandler ajaxCsrfCheckFailureHandler() {
        if (null == ajaxCsrfCheckFailureHandler) {
            ajaxCsrfCheckFailureHandler = get(HANDLER_AJAX_CSRF_CHECK_FAILURE, csrfCheckFailureHandler());
        }
        return ajaxCsrfCheckFailureHandler;
    }

    private void _mergeAjaxCsrfCheckFailureHandler(AppConfig config) {
        if (!hasConfiguration(HANDLER_AJAX_CSRF_CHECK_FAILURE)) {
            csrfCheckFailureHandler = config.csrfCheckFailureHandler;
        }
    }

    private Integer threadlocalBufRetentionLimit;

    protected T threadLocalBufRetentionLimit(int limit) {
        threadlocalBufRetentionLimit = limit;
        return me();
    }

    public int threadLocalBufRetentionLimit() {
        if (null == threadlocalBufRetentionLimit) {
            StrBufRetentionLimitCalculator calc = new StrBufRetentionLimitCalculator();
            threadlocalBufRetentionLimit = get(OSGL_THREADLOCAL_BUF_LIMIT, 1024 * calc.calculate());
        }
        return threadlocalBufRetentionLimit;
    }

    private void _mergeStrBufRetentionLimit(AppConfig config) {
        if (!hasConfiguration(OSGL_THREADLOCAL_BUF_LIMIT)) {
            threadlocalBufRetentionLimit = config.threadlocalBufRetentionLimit;
        }
    }

    private Password.Validator defPasswordValidator;

    protected T defPasswordValidator(Password.Validator validator) {
        defPasswordValidator = $.requireNotNull(validator);
        return me();
    }

    protected T defPasswordSpec(String spec) {
        _defPasswordSpec(spec);
        return me();
    }

    public Password.Validator defPasswordValidator() {
        if (null == defPasswordValidator) {
            String s = get(PASSWORD_DEF_SPEC, Act.isDev() ? "a[3,]" : "aA0[6,]");
            _defPasswordSpec(s);
        }
        return defPasswordValidator;
    }

    private void _defPasswordSpec(String spec) {
        try {
            defPasswordValidator = PasswordSpec.parse(spec);
        } catch (IllegalArgumentException e) {
            // try to check if the spec is a PasswordValidator
            try {
                defPasswordValidator = app.getInstance(spec);
            } catch (Exception e2) {
                throw new ConfigurationException("Password spec unrecognized: " + spec);
            }
        }
    }

    private Boolean monitorEnabled;

    protected T enableMonitor(boolean enable) {
        this.monitorEnabled = enable;
        return me();
    }

    public boolean monitorEnabled() {
        if (null == monitorEnabled) {
            monitorEnabled = $.bool(get(MONITOR, false));
        }
        return monitorEnabled;
    }

    private void _mergeMonitorEnabled(AppConfig config) {
        if (!hasConfiguration(MONITOR)) {
            this.monitorEnabled = config.monitorEnabled;
        }
    }

    private Map<String, NamedPort> namedPorts = null;

    protected T namedPorts(NamedPort... namedPorts) {
        this.namedPorts = new HashMap<>();
        for (NamedPort port : namedPorts) {
            this.namedPorts.put(port.name(), port);
        }
        return me();
    }

    public Collection<NamedPort> namedPorts() {
        if (null == namedPorts) {
            String s = get(NAMED_PORTS, null);
            if (null == s) {
                namedPorts = new HashMap<>();
            } else {
                String[] sa = (s.split("[,;]+"));
                Map<String, NamedPort> builder = new HashMap<>();
                for (String s0 : sa) {
                    String[] sa0 = s0.split(":");
                    E.invalidConfigurationIf(2 != sa0.length, "Unknown named port configuration: %s", s);
                    String name = sa0[0].trim();
                    String val = sa0[1].trim();
                    NamedPort port = new NamedPort(name, Integer.parseInt(val));
                    if (!builder.containsKey(port.name())) {
                        builder.put(port.name(), port);
                    } else {
                        throw E.invalidConfiguration("port[%s] already configured", name);
                    }
                }
                namedPorts = builder;
            }
            if (cliOverHttp() && !namedPorts.containsKey(PORT_CLI_OVER_HTTP)) {
                namedPorts.put(PORT_CLI_OVER_HTTP, new NamedPort(PORT_CLI_OVER_HTTP, cliOverHttpPort()));
            }
            if (!namedPorts.containsKey(PORT_SYS)) {
                namedPorts.put(PORT_SYS, new NamedPort(PORT_SYS, httpPort() + 3));
            }

        }
        return namedPorts.values();
    }

    public NamedPort namedPort(String portId) {
        for (NamedPort np : namedPorts()) {
            if (np.name().equalsIgnoreCase(portId)) {
                return np;
            }
        }
        return null;
    }

    private void _mergePorts(AppConfig config) {
        if (!hasConfiguration(NAMED_PORTS)) {
            namedPorts = config.namedPorts;
        }
    }

    private String encoding = null;

    protected T encoding(String encoding) {
        encoding = encoding.trim();
        E.illegalArgumentIf(encoding.length() == 0, "encoding cannot be empty");
        this.encoding = encoding;
        return me();
    }

    public String encoding() {
        if (null == encoding) {
            encoding = get(ENCODING, StandardCharsets.UTF_8.name());
        }
        return encoding;
    }

    private void _mergeEncoding(AppConfig conf) {
        if (!hasConfiguration(ENCODING)) {
            encoding = conf.encoding;
        }
    }

    private volatile String datePattern = null;
    private SimpleDateFormat dateFormat = null;
    private final ThreadLocal<SimpleDateFormat> dateFormatThreadLocal = new ThreadLocal<SimpleDateFormat>() {
        @Override
        protected SimpleDateFormat initialValue() {
            if (null == dateFormat) {
                String pattern = datePattern();
                if (null == dateFormat) {
                    dateFormat = new SimpleDateFormat(pattern);
                }
            }
            return (SimpleDateFormat) dateFormat.clone();
        }
    };

    protected T datePattern(String fmt) {
        E.illegalArgumentIf(S.blank(fmt), "Date format pattern cannot be empty");
        this.datePattern = fmt;
        return me();
    }

    public SimpleDateFormat dateFormat() {
        return dateFormatThreadLocal.get();
    }

    public String datePattern() {
        if (null == datePattern) {
            synchronized (this) {
                if (null == datePattern) {
                    dateStyle = DateTimeStyle.MEDIUM;
                    datePattern = get(FORMAT_DATE, null);
                    if (null == datePattern) {
                        dateFormat = (SimpleDateFormat) DateFormat.getDateInstance(DateFormat.DEFAULT, locale());
                    } else if (S.eq(datePattern, "long", S.IGNORECASE)) {
                        dateStyle = DateTimeStyle.LONG;
                        dateFormat = (SimpleDateFormat) DateFormat.getDateInstance(DateFormat.LONG, locale());
                    } else if (S.eq(datePattern, "medium", S.IGNORECASE)) {
                        dateStyle = DateTimeStyle.MEDIUM;
                        dateFormat = (SimpleDateFormat) DateFormat.getDateInstance(DateFormat.MEDIUM, locale());
                    } else if (S.eq(datePattern, "short", S.IGNORECASE)) {
                        dateStyle = DateTimeStyle.SHORT;
                        dateFormat = (SimpleDateFormat) DateFormat.getDateInstance(DateFormat.SHORT, locale());
                    }
                    if (null != dateFormat) {
                        datePattern = dateFormat.toPattern();
                    }
                }
            }
        }
        return datePattern;
    }

    private void _mergeDatePattern(AppConfig conf) {
        if (!hasConfiguration(FORMAT_DATE)) {
            datePattern = conf.datePattern;
            dateStyle = conf.dateStyle;
        }
    }

    private Map<Locale, String> localizedDatePatterns = new HashMap<>();

    public String localizedDatePattern(Locale locale) {
        String s = localizedDatePatterns.get(locale);
        if (null == s) {
            s = getLocalizedDateTimePattern(locale, DateTimeType.DATE);
            localizedDatePatterns.put(locale, s);
        }
        return s;
    }


    private volatile String timePattern = null;
    private SimpleDateFormat timeFormat = null;
    private final ThreadLocal<SimpleDateFormat> timeFormatThreadLocal = new ThreadLocal<SimpleDateFormat>() {
        @Override
        protected SimpleDateFormat initialValue() {
            if (null == timeFormat) {
                String pattern = timePattern();
                if (null == timeFormat) {
                    timeFormat = new SimpleDateFormat(pattern);
                }
            }
            return (SimpleDateFormat) timeFormat.clone();
        }
    };

    protected T timePattern(String pattern) {
        E.illegalArgumentIf(S.blank(pattern), "Time format pattern cannot be empty");
        this.timePattern = pattern;
        return me();
    }

    public SimpleDateFormat timeFormat() {
        return timeFormatThreadLocal.get();
    }

    public String timePattern() {
        if (null == timePattern) {
            synchronized (this) {
                if (null == timePattern) {
                    timeStyle = DateTimeStyle.MEDIUM;
                    timePattern = get(FORMAT_TIME, null);
                    if (null == timePattern) {
                        timeFormat = (SimpleDateFormat) DateFormat.getTimeInstance(DateFormat.DEFAULT, locale());
                    } else if (S.eq(timePattern, "long", S.IGNORECASE)) {
                        timeStyle = DateTimeStyle.LONG;
                        timeFormat = (SimpleDateFormat) DateFormat.getTimeInstance(DateFormat.LONG, locale());
                    } else if (S.eq(timePattern, "medium", S.IGNORECASE)) {
                        timeStyle = DateTimeStyle.MEDIUM;
                        timeFormat = (SimpleDateFormat) DateFormat.getTimeInstance(DateFormat.MEDIUM, locale());
                    } else if (S.eq(timePattern, "short", S.IGNORECASE)) {
                        timeStyle = DateTimeStyle.SHORT;
                        timeFormat = (SimpleDateFormat) DateFormat.getTimeInstance(DateFormat.SHORT, locale());
                    }

                    if (null != timeFormat) {
                        timePattern = timeFormat.toPattern();
                    }
                }
            }
        }
        return timePattern;
    }

    private void _mergeTimePattern(AppConfig conf) {
        if (!hasConfiguration(FORMAT_TIME)) {
            timePattern = conf.timePattern;
            timeStyle = conf.timeStyle;
        }
    }

    private Map<Locale, String> localizedTimePattern = new HashMap<>();

    public String localizedTimePattern(Locale locale) {
        String s = localizedTimePattern.get(locale);
        if (null == s) {
            s = getLocalizedDateTimePattern(locale, DateTimeType.TIME);
            localizedTimePattern.put(locale, s);
        }
        return s;
    }


    private volatile String dateTimePattern = null;
    private SimpleDateFormat dateTimeFormat = null;
    private final ThreadLocal<SimpleDateFormat> dateTimeFormatThreadLocal = new ThreadLocal<SimpleDateFormat>() {
        @Override
        protected SimpleDateFormat initialValue() {
            if (null == dateTimeFormat) {
                String pattern = dateTimePattern();
                if (null == dateTimeFormat) {
                    dateTimeFormat = new SimpleDateFormat(pattern);
                }
            }
            return (SimpleDateFormat) dateTimeFormat.clone();
        }
    };

    protected T dateTimePattern(String pattern) {
        E.illegalArgumentIf(S.blank(pattern), "Date time format pattern cannot be empty");
        this.dateTimePattern = pattern;
        return me();
    }

    public SimpleDateFormat dateTimeFormat() {
        return dateTimeFormatThreadLocal.get();
    }

    public String dateTimePattern() {
        if (null == dateTimePattern) {
            synchronized (this) {
                if (null == dateTimePattern) {
                    dateTimeStyle = DateTimeStyle.MEDIUM;
                    dateTimePattern = get(FORMAT_DATE_TIME, null);
                    if (null == dateTimePattern) {
                        dateTimeFormat = (SimpleDateFormat) DateFormat.getDateTimeInstance(DateFormat.DEFAULT, DateFormat.DEFAULT, locale());
                    } else if (S.eq(dateTimePattern, "long", S.IGNORECASE)) {
                        dateTimeStyle = DateTimeStyle.LONG;
                        dateTimeFormat = (SimpleDateFormat) DateFormat.getDateTimeInstance(DateFormat.LONG, DateFormat.LONG, locale());
                    } else if (S.eq(dateTimePattern, "medium", S.IGNORECASE)) {
                        dateTimeStyle = DateTimeStyle.MEDIUM;
                        dateTimeFormat = (SimpleDateFormat) DateFormat.getDateTimeInstance(DateFormat.MEDIUM, DateFormat.MEDIUM, locale());
                    } else if (S.eq(dateTimePattern, "short", S.IGNORECASE)) {
                        dateTimeStyle = DateTimeStyle.SHORT;
                        dateTimeFormat = (SimpleDateFormat) DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.SHORT, locale());
                    }
                    if (null != dateTimeFormat) {
                        dateTimePattern = dateTimeFormat.toPattern();
                    }
                }
            }
        }
        return dateTimePattern;
    }

    private void _mergeDateTimeFmt(AppConfig conf) {
        if (!hasConfiguration(FORMAT_DATE_TIME)) {
            dateTimePattern = conf.dateTimePattern;
        }
    }

    private Map<Locale, String> localizedDateTimePatterns = new HashMap<>();

    public String localizedDateTimePattern(Locale locale) {
        String s = localizedDateTimePatterns.get(locale);
        if (null == s) {
            s = getLocalizedDateTimePattern(locale, DateTimeType.DATE_TIME);
            localizedDateTimePatterns.put(locale, s);
        }
        return s;
    }

    private boolean isLocaleMatchesDefault(Locale locale) {
        return locale().equals(locale) || baseLocale.equals(locale);
    }

    private Locale locale = null;
    private Locale baseLocale = null;

    protected T locale(Locale locale) {
        this.baseLocale = baseLocaleOf(locale);
        this.locale = locale;
        return me();
    }

    public Locale locale() {
        if (null == locale) {
            locale(get(LOCALE, Locale.getDefault()));
        }
        return locale;
    }

    private void _mergeLocale(AppConfig conf) {
        if (!hasConfiguration(LOCALE)) {
            locale = conf.locale;
            baseLocale = conf.baseLocale;
        }
    }

    private Locale baseLocaleOf(Locale locale) {
        return new Locale.Builder().setLanguage(locale.getLanguage()).build();
    }

    private String sourceVersion = null;

    protected T sourceVersion(JavaVersion version) {
        sourceVersion = FastStr.of(version.name()).substr(1).replace('_', '.').toString();
        return me();
    }

    public String sourceVersion() {
        if (null == sourceVersion) {
            sourceVersion = get(SOURCE_VERSION, S.string($.JAVA_VERSION));
//            sourceVersion = get(AppConfigKey.SOURCE_VERSION, null);
//            if (null == sourceVersion) {
//                int n = $.JAVA_VERSION;
//                if (n > 8) {
//                    warn("ActFramework support compiling source code up to Java 8 only");
//                    n = 8;
//                }
//                sourceVersion = S.string(n);
//            } else {
//                if (sourceVersion.contains("1.")) {
//                    sourceVersion = sourceVersion.substring(2);
//                }
//                if (Integer.parseInt(sourceVersion) > 8) {
//                    warn("ActFramework support compiling source code up to Java 8 only");
//                    sourceVersion = "8";
//                }
//            }
        }
        return sourceVersion;
    }

    private void _mergeSourceVersion(AppConfig conf) {
        if (!hasConfiguration(SOURCE_VERSION)) {
            sourceVersion = conf.sourceVersion;
        }
    }

    private Boolean selfHealing;
    protected T selfHealing(boolean on) {
        selfHealing = on;
        return me();
    }
    public boolean selfHealing() {
        if (null == selfHealing) {
            selfHealing = get(SYS_SELF_HEALING, false);
        }
        return selfHealing;
    }
    private void _mergeSelfHealing(AppConfig conf) {
        if (!hasConfiguration(SYS_SELF_HEALING)) {
            selfHealing = conf.selfHealing;
        }
    }

    private String targetVersion = null;

    protected T targetVersion(JavaVersion version) {
        targetVersion = FastStr.of(version.name()).substr(1).replace('_', '.').toString();
        return me();
    }

    public String targetVersion() {
        if (null == targetVersion) {
            targetVersion = get(TARGET_VERSION, null);
            if (null == targetVersion) {
                int n = $.JAVA_VERSION;
                if (n > 8) {
                    warn("ActFramework support compiling source code up to Java 8 only");
                    n = 8;
                }
                targetVersion = S.string(n);
            } else {
                if (targetVersion.contains("1.")) {
                    targetVersion = targetVersion.substring(2);
                }
                if (Integer.parseInt(targetVersion) > 8) {
                    warn("ActFramework support compiling source code up to Java 8 only");
                    targetVersion = "8";
                }
            }
        }
        return targetVersion;
    }

    private void _mergeTargetVersion(AppConfig conf) {
        if (!hasConfiguration(TARGET_VERSION)) {
            targetVersion = conf.targetVersion;
        }
    }

    private $.Predicate<String> APP_CLASS_TESTER = null;
    private final $.Predicate<String> SYSTEM_SCAN_LIST = new $.Predicate<String>() {
        @Override
        public boolean test(String s) {
            final Set<String> scanList = app().scanList();
            if (scanList.contains(s)) {
                return true;
            }
            if (s.startsWith("java.") || s.startsWith("javax.")) {
                return false;
            }
            if (s.contains("$")) {
                for (String pkg: scanList) {
                    if (s.startsWith(pkg + "$")) {
                        return true;
                    }
                }
            }
            boolean shouldCheckPattern = false;
            Set<String> prefixList = app().scanPrefixList();
            for (String prefix : prefixList) {
                if (s.startsWith(prefix)) {
                    shouldCheckPattern = true;
                    break;
                }
            }
            if (!shouldCheckPattern) {
                return false;
            }
            shouldCheckPattern = false;
            Set<String> suffixList = app().scanSuffixList();
            for (String suffix : suffixList) {
                if (s.endsWith(suffix)) {
                    shouldCheckPattern = true;
                    break;
                }
            }
            if (!shouldCheckPattern) {
                return false;
            }
            for (Pattern pattern : app().scanPattern()) {
                if (pattern.matcher(s).matches()) {
                    return true;
                }
            }
            return false;
        }
    };

    public $.Predicate<String> appClassTester() {
        if (null == APP_CLASS_TESTER) {
            String scanPackage = get(AppConfigKey.SCAN_PACKAGE, null);
            if (S.isBlank(scanPackage)) {
                APP_CLASS_TESTER = SYSTEM_SCAN_LIST;
            } else {
                final String[] sp = scanPackage.trim().split(Constants.LIST_SEPARATOR);
                final int len = sp.length;
                for (int i = 0; i < len; ++i) {
                    String pkg = sp[i];
                    if (pkg.startsWith("act.") || "act".equals(pkg)) {
                        throw new ConfigurationException("Scan package cannot be 'act' or starts with 'act.'");
                    }
                }
                if (1 == len) {
                    APP_CLASS_TESTER = S.F.startsWith(sp[0]).or(SYSTEM_SCAN_LIST);
                } else {
                    APP_CLASS_TESTER = new $.Predicate<String>() {
                        @Override
                        public boolean test(String className) {
                            for (int i = 0; i < len; ++i) {
                                if (className.startsWith(sp[i])) {
                                    return true;
                                }
                            }
                            return false;
                        }
                    }.or(SYSTEM_SCAN_LIST);
                }
            }
        }
        return APP_CLASS_TESTER;
    }

    public boolean needEnhancement(String className) {
        return appClassTester().test(className) || controllerNameTester().test(className);
    }

    private $.Predicate<String> CONTROLLER_CLASS_TESTER = null;

    private $.Predicate<String> controllerNameTester() {
        if (null == CONTROLLER_CLASS_TESTER) {
            $.Predicate<String> f = $.F.no();
            CONTROLLER_CLASS_TESTER = f.or(app().router().f.IS_CONTROLLER);
        }
        return CONTROLLER_CLASS_TESTER;
    }

    private Integer testTimeout;

    protected T testTimeout(int timeout) {
        if (timeout < 10) {
            logger.warn("test.timeout reset to minimum value: 10");
            timeout = 10;
        }
        testTimeout = timeout;
        return me();
    }

    public int testTimeout() {
        if (null == testTimeout) {
            int defTimeout = ("e2e".equalsIgnoreCase(Act.profile()) || "test".equalsIgnoreCase(Act.profile())) ? 10 : 60 * 60;
            testTimeout = get(TEST_TIMEOUT, defTimeout);
            this.testTimeout(testTimeout); // make sure we have a valid number
        }
        return testTimeout;
    }

    private void _mergeTestTimeout(AppConfig conf) {
        if (!hasConfiguration(TEST_TIMEOUT)) {
            testTimeout = conf.testTimeout;
        }
    }

    private TemplatePathResolver templatePathResolver;

    protected T templatePathResolver(TemplatePathResolver resolver) {
        E.NPE(resolver);
        templatePathResolver = resolver;
        return me();
    }

    public TemplatePathResolver templatePathResolver() {
        if (null == templatePathResolver) {
            templatePathResolver = get(AppConfigKey.RESOLVER_TEMPLATE_PATH, new TemplatePathResolver());
        }
        return templatePathResolver;
    }

    private void _mergeTemplatePathResolver(AppConfig conf) {
        if (!hasConfiguration(AppConfigKey.RESOLVER_TEMPLATE_PATH)) {
            templatePathResolver = conf.templatePathResolver;
        }
    }

    private String templateHome = null;

    protected T templateHome(String home) {
        home = home.trim();
        E.illegalArgumentIf(home.length() == 0, "template home cannot be empty");
        templateHome = home;
        return me();
    }

    public String templateHome() {
        if (null == templateHome) {
            templateHome = get(AppConfigKey.TEMPLATE_HOME, "default");
        }
        return templateHome;
    }

    private void _mergeTemplateHome(AppConfig conf) {
        if (!hasConfiguration(AppConfigKey.TEMPLATE_HOME)) {
            templateHome = conf.templateHome;
        }
    }

    private Boolean paramBindingKeywordMatching;

    protected T paramBindingKeywordMatching(boolean enabled) {
        paramBindingKeywordMatching = enabled;
        return me();
    }

    public boolean paramBindingKeywordMatching() {
        if (null == paramBindingKeywordMatching) {
            paramBindingKeywordMatching = get(PARAM_BINDING_KEYWORD_MATCHING, false);
        }
        return paramBindingKeywordMatching;
    }

    private void _mergeParamBindingKeywordMatching(AppConfig config) {
        if (!hasConfiguration(PARAM_BINDING_KEYWORD_MATCHING)) {
            paramBindingKeywordMatching = config.paramBindingKeywordMatching;
        }
    }

    private boolean pingPathResolved = false;
    private String pingPath = null;

    protected T pingPath(String path) {
        pingPathResolved = true;
        pingPath = path.trim();
        return me();
    }

    public String pingPath() {
        if (!pingPathResolved) {
            pingPath = get(AppConfigKey.PING_PATH, null);
            pingPathResolved = true;
        }
        return pingPath;
    }

    private void _mergePingPath(AppConfig config) {
        if (!hasConfiguration(AppConfigKey.PING_PATH)) {
            pingPath = config.pingPath;
            pingPathResolved = config.pingPathResolved;
        }
    }

    private Integer reqThrottle;

    protected T requestThrottle(final int throttle) {
        E.illegalArgumentIf(throttle < 1, "request throttle must be positive integer");
        this.reqThrottle = throttle;
        return me();
    }

    public int requestThrottle() {
        if (null == reqThrottle) {
            reqThrottle = get(REQUEST_THROTTLE, 2);
        }
        return reqThrottle;
    }

    private void _mergeReqThrottle(AppConfig config) {
        if (!hasConfiguration(REQUEST_THROTTLE)) {
            this.reqThrottle = config.reqThrottle;
        }
    }

    private Boolean reqThrottleExpireScale;

    protected T requestThrottleExpireScale(final boolean enabled) {
        this.reqThrottleExpireScale = enabled;
        return me();
    }

    public boolean requestThrottleExpireScale() {
        if (null == reqThrottleExpireScale) {
            reqThrottleExpireScale = get(REQUEST_THROTTLE_EXPIRE_SCALE, false);
        }
        return reqThrottleExpireScale;
    }

    private void _mergeReqThrottleExpireScale(AppConfig config) {
        if (!hasConfiguration(REQUEST_THROTTLE_EXPIRE_SCALE)) {
            this.reqThrottleExpireScale = config.reqThrottleExpireScale;
        }
    }

    private $.Func0<H.Format> jsonContentTypeProvider = null;
    private Boolean renderJsonIeFix = null;
    private H.Format jsonIE;

    protected T renderJsonContentTypeIE(final String contentType) {
        setRenderJsonContenTypeIE(contentType);
        return me();
    }

    private void setRenderJsonContenTypeIE(final String contentType) {
        if (H.Format.JSON.contentType().equalsIgnoreCase(contentType)) {
            renderJsonIeFix = false;
            return;
        }
        renderJsonIeFix = true;
        jsonIE = H.Format.of("json_ie", contentType);
        jsonContentTypeProvider = new $.Func0<H.Format>() {
            @Override
            public H.Format apply() throws NotAppliedException, $.Break {
                ActionContext context = ActionContext.current();
                if (null != context) {
                    UserAgent ua = context.userAgent();
                    if (ua.isIE()) {
                        return jsonIE;
                    }
                }
                return H.Format.JSON;
            }
        };
    }

    public $.Func0<H.Format> jsonContentTypeProvider() {
        if (null == renderJsonIeFix) {
            String contentType = get(RENDER_JSON_CONTENT_TYPE_IE, null);
            if (null != contentType) {
                setRenderJsonContenTypeIE(contentType);
            } else {
                renderJsonIeFix = false;
            }
        }
        return renderJsonIeFix ? jsonContentTypeProvider : null;
    }

    private void _mergeRenderJsonContentTypeIE(AppConfig conf) {
        if (!hasConfiguration(RENDER_JSON_CONTENT_TYPE_IE)) {
            jsonContentTypeProvider = conf.jsonContentTypeProvider;
            renderJsonIeFix = conf.renderJsonIeFix;
            jsonIE = conf.jsonIE;
        }
    }

    private Boolean renderJsonOutputCharset;

    protected T renderJsonOutputCharset(boolean outputCharset) {
        this.renderJsonOutputCharset = outputCharset;
        return me();
    }

    public boolean renderJsonOutputCharset() {
        if (null == renderJsonOutputCharset) {
            renderJsonOutputCharset = get(RENDER_JSON_OUTPUT_CHARSET, false);
        }
        return renderJsonOutputCharset;
    }

    private void _mergeRenderJsonOutputCharset(AppConfig config) {
        if (!hasConfiguration(RENDER_JSON_OUTPUT_CHARSET)) {
            renderJsonOutputCharset = config.renderJsonOutputCharset;
        }
    }

    private String serverHeader;
    private static final String DEF_SERVER_HEADER = "act/" + Act.VERSION.getProjectVersion();
    private static String DEF_APP_SERVER_HEADER = appServerHeader();
    private static String appServerHeader() {
        App app = Act.app();
        if (null == app) {
            return "app/1.0";
        }
        String shortId = app.shortId();
        if (null == shortId) {
            shortId = "app";
        }
        Version version = app.version();
        return shortId + "/" + (null == version ? "1.0" : version.getProjectVersion());
    }

    protected T serverHeader(String header) {
        serverHeader = header;
        return me();
    }

    public String serverHeader() {
        if (null == serverHeader) {
            serverHeader = get(AppConfigKey.SERVER_HEADER, serverHeaderUseApp() ? DEF_APP_SERVER_HEADER : DEF_SERVER_HEADER);
        }
        return serverHeader;
    }

    private void _mergeServerHeader(AppConfig config) {
        if (!hasConfiguration(AppConfigKey.SERVER_HEADER)) {
            serverHeader = config.serverHeader;
        }
    }

    private Boolean serverHeaderUseApp;
    protected T serverHeaderUseApp(boolean b) {
        serverHeaderUseApp = b;
        return me();
    }
    private boolean serverHeaderUseApp() {
        if (null == serverHeaderUseApp) {
            serverHeaderUseApp = get(AppConfigKey.SERVER_HEADER_USE_APP, true);
        }
        return serverHeaderUseApp;
    }
    private void _mergeServerHeaderUseApp(AppConfig config) {
        if (!hasConfiguration(SERVER_HEADER_USE_APP)) {
            serverHeaderUseApp = config.serverHeaderUseApp;
        }
    }

    private String sessionKeyUsername = null;

    protected T sessionKeyUsername(String name) {
        name = name.trim().toLowerCase();
        E.illegalArgumentIf(name.length() == 0, "session cookie name cannot be blank");
        sessionKeyUsername = name;
        return me();
    }

    public String sessionKeyUsername() {
        if (null == sessionKeyUsername) {
            sessionKeyUsername = get(SESSION_KEY_USERNAME, "username");
        }
        return sessionKeyUsername;
    }

    private void _mergeSessionKeyUsername(AppConfig config) {
        if (!hasConfiguration(SESSION_KEY_USERNAME)) {
            sessionKeyUsername = config.sessionKeyUsername;
        }
    }

    private String cookiePrefix;

    protected T cookiePrefix(String prefix) {
        prefix = prefix.trim().toLowerCase();
        E.illegalArgumentIf(prefix.length() == 0, "cookie prefix cannot be blank");
        cookiePrefix = prefix;
        return me();
    }

    private String cookiePrefix() {
        if (null == cookiePrefix) {
            String profile = Act.profile();
            S.Buffer buf = S.buffer(app().shortId());
            if (S.neq("prod", profile, S.IGNORECASE)) {
                buf.a("-").a(profile);
            }
            buf.a("-");
            cookiePrefix = get(COOKIE_PREFIX, buf.toString());
            cookiePrefix = cookiePrefix.trim().toLowerCase();
            set(COOKIE_PREFIX, cookiePrefix);
        }
        return cookiePrefix;
    }

    private void _mergeCookiePrefix(AppConfig config) {
        if (!hasConfiguration(COOKIE_PREFIX)) {
            cookiePrefix = config.cookiePrefix;
        }
    }

    private String sessionCookieName = null;

    protected T sessionCookieName(String name) {
        name = name.trim().toLowerCase();
        E.illegalArgumentIf(name.length() == 0, "session cookie name cannot be blank");
        E.illegalArgumentIf(S.eq(name, flashCookieName), "session cookie name cannot be the same with flash cookie name");
        sessionCookieName = name;
        return me();
    }

    public String sessionCookieName() {
        if (null == sessionCookieName) {
            sessionCookieName = cookieName("session");
        }
        return sessionCookieName;
    }

    private void _mergeSessionCookieName(AppConfig config) {
        if (null != config.sessionCookieName) {
            sessionCookieName = config.sessionCookieName;
        }
    }

    private String flashCookieName = null;

    protected T flashCookieName(String name) {
        name = name.trim().toLowerCase();
        E.illegalArgumentIf(name.length() == 0, "flash cookie name cannot be blank");
        E.illegalArgumentIf(S.eq(name, sessionCookieName), "flash cookie name cannot be the same with session cookie name");
        flashCookieName = name;
        return me();
    }

    public String flashCookieName() {
        if (null == flashCookieName) {
            flashCookieName = cookieName("flash");
        }
        return flashCookieName;
    }

    private void _mergeFlashCookieName(AppConfig config) {
        if (null != config.flashCookieName) {
            flashCookieName = config.flashCookieName;
        }
    }

    private Integer sessionTtl = null;

    protected T sessionTtl(int seconds) {
        sessionTtl = seconds;
        return me();
    }

    public int sessionTtl() {
        if (null == sessionTtl) {
            sessionTtl = get(AppConfigKey.SESSION_TTL, 60 * 30);
        }
        return sessionTtl;
    }

    private void _mergeSessionTtl(AppConfig conf) {
        if (!hasConfiguration(AppConfigKey.SESSION_TTL)) {
            sessionTtl = conf.sessionTtl;
        }
    }

    private boolean sessionPassThrough;
    private boolean sessionPassThroughSet; // use this to save auto-box of sessionPassThrough flag
    protected T sessionPassThrough(boolean b) {
        sessionPassThrough = b;
        return me();
    }
    public boolean sessionPassThrough() {
        if (!sessionPassThroughSet) {
            sessionPassThrough = get(SESSION_PASS_THROUGH, false);
        }
        return sessionPassThrough;
    }
    private void _mergeSessionPassThrough(AppConfig config) {
        if (!hasConfiguration(SESSION_PASS_THROUGH)) {
            sessionPassThrough = config.sessionPassThrough;
        }
    }

    private Boolean sessionPersistent;

    protected T sessionPersistent(boolean persistenSession) {
        sessionPersistent = persistenSession;
        return me();
    }

    public boolean persistSession() {
        if (null == sessionPersistent) {
            sessionPersistent = get(AppConfigKey.SESSION_PERSISTENT_ENABLED, false);
        }
        return sessionPersistent;
    }

    private void _mergeSessionPersistent(AppConfig config) {
        if (!hasConfiguration(AppConfigKey.SESSION_PERSISTENT_ENABLED)) {
            sessionPersistent = config.sessionPersistent;
        }
    }

    private Boolean sessionEncrypt = null;

    protected T sessionEncrypt(boolean encryptSession) {
        sessionEncrypt = encryptSession;
        return me();
    }

    public boolean encryptSession() {
        if (null == sessionEncrypt) {
            sessionEncrypt = get(AppConfigKey.SESSION_ENCRYPT_ENABLED, false);
        }
        return sessionEncrypt;
    }

    private void _mergeSessionEncrpt(AppConfig config) {
        if (!hasConfiguration(AppConfigKey.SESSION_ENCRYPT_ENABLED)) {
            sessionEncrypt = config.sessionEncrypt;
        }
    }

    private act.session.SessionMapper sessionMapper = null;

    protected void sessionMapper(act.session.SessionMapper sessionMapper) {
        this.sessionMapper = sessionMapper;
    }

    @Lazy
    public act.session.SessionMapper sessionMapper() {
        if (null == sessionMapper && null != app.injector()) {
            sessionMapper = get(SESSION_MAPPER, app().getInstance(CookieSessionMapper.class));
        }
        return sessionMapper;
    }

    private void _mergeSessionMapper(AppConfig config) {
        if (!hasConfiguration(AppConfigKey.SESSION_MAPPER)) {
            sessionMapper = config.sessionMapper;
        }
    }

    private SessionCodec sessionCodec = null;

    protected void sessionCodec(SessionCodec codec) {
        this.sessionCodec = $.requireNotNull(codec);
    }

    @Lazy
    public SessionCodec sessionCodec() {
        if (null == sessionCodec) {
            if (null == app.injector()) {
                // unit testing
                sessionCodec = null;
            } else {
                sessionCodec = get(SESSION_CODEC, app.getInstance(DefaultSessionCodec.class));
            }
        }
        return sessionCodec;
    }

    private void _mergeSessionCodec(AppConfig config) {
        if (!hasConfiguration(AppConfigKey.SESSION_CODEC)) {
            sessionCodec = config.sessionCodec;
        }
    }

    private Boolean sessionOutputExpiration;

    protected T sessionOutputExpiration(boolean enabled) {
        sessionOutputExpiration = enabled;
        return me();
    }

    public boolean sessionOutputExpiration() {
        if (null == sessionOutputExpiration) {
            sessionOutputExpiration = get(SESSION_OUTPUT_EXPIRATION, true);
        }
        return sessionOutputExpiration;
    }

    private String sessionHeader;
    private boolean sessionHeaderSet;

    protected T sessionHeader(String header) {
        this.sessionHeader = header;
        this.sessionHeaderSet = true;
        return me();
    }

    public String sessionHeader() {
        if (!sessionHeaderSet) {
            sessionHeader = get(SESSION_HEADER, S.pathConcat(sessionHeaderPrefix(), '-', "Session"));
            sessionHeaderSet = true;
        }
        return sessionHeader;
    }

    private void _mergeSessionHeader(AppConfig conf) {
        if (!hasConfiguration(SESSION_HEADER)) {
            sessionHeader = conf.sessionHeader;
            sessionHeaderSet = conf.sessionHeaderSet;
        }
    }

    private String sessionHeaderPrefix;

    protected T sessionHeaderPrefix(String prefix) {
        this.sessionHeaderPrefix = prefix;
        return me();
    }

    public String sessionHeaderPrefix() {
        if (null == sessionHeaderPrefix) {
            sessionHeaderPrefix = get(SESSION_HEADER_PREFIX, HeaderTokenSessionMapper.DEF_HEADER_PREFIX);
        }
        return sessionHeaderPrefix;
    }

    private String sessionHeaderPayloadPrefix = null;

    protected void sessionHeaderPayloadPrefix(String prefix) {
        this.sessionHeaderPayloadPrefix = prefix;
    }

    public String sessionHeaderPayloadPrefix() {
        if (null == sessionHeaderPayloadPrefix) {
            String s = get(SESSION_HEADER_PAYLOAD_PREFIX, HeaderTokenSessionMapper.DEF_PAYLOAD_PREFIX);
            s = S.strip(s).of(S.DOUBLE_QUOTES);
            sessionHeaderPayloadPrefix = s;
        }
        return sessionHeaderPayloadPrefix;
    }

    private void _mergeSessionHeaderPayloadPrefix(AppConfig config) {
        if (!hasConfiguration(AppConfigKey.SESSION_HEADER_PAYLOAD_PREFIX)) {
            sessionHeaderPayloadPrefix = config.sessionHeaderPayloadPrefix;
        }
    }

    private String sessionQueryParamName;
    protected T sessionQueryParamName(String paramName) {
        sessionQueryParamName = paramName;
        return me();
    }
    public String getSessionQueryParamName() {
        if (null == sessionQueryParamName) {
            sessionQueryParamName = get(SESSION_QUERY_PARAM_NAME, sessionHeader());
        }
        return sessionQueryParamName;
    }
    private void _mergeSessionQueryParamName(AppConfig config) {
        if (!hasConfiguration(SESSION_QUERY_PARAM_NAME)) {
            sessionQueryParamName = config.sessionQueryParamName;
        }
    }

    private Boolean sessionSecure = null;

    protected T sessionSecure(boolean secure) {
        sessionSecure = secure;
        return me();
    }

    public boolean sessionSecure() {
        if (null == sessionSecure) {
            sessionSecure = get(AppConfigKey.SESSION_SECURE, httpSecure());
        }
        return sessionSecure;
    }

    private void _mergeSessionSecure(AppConfig config) {
        if (!hasConfiguration(AppConfigKey.SESSION_SECURE)) {
            sessionSecure = config.sessionSecure;
        }
    }

    private volatile String secret = null;

    protected T secret(String secret) {
        E.illegalArgumentIf(S.blank(secret));
        this.secret = secret;
        return me();
    }

    public String secret() {
        if (null == secret) {
            secret = get(AppConfigKey.SECRET, "myawesomeapp");
            if ("myawesomeapp".equals(secret)) {
                logger.warn("Application secret key not set! You are in the dangerous zone!!!");
            }
        }
        return secret;
    }

    private void _mergeSecret(AppConfig config) {
        if (!hasConfiguration(AppConfigKey.SECRET)) {
            secret = config.secret;
        }
    }

    private Boolean secretRotate = null;

    protected T secretRotate(boolean enabled) {
        secretRotate = enabled;
        return me();
    }

    public boolean rotateSecret() {
        if (null == secretRotate) {
            secretRotate = get(SECRET_ROTATE, false);
        }
        return secretRotate;
    }

    private void _mergeSecretRotate(AppConfig config) {
        if (!hasConfiguration(SECRET_ROTATE)) {
            secretRotate = config.secretRotate;
        }
    }

    private Integer secretRotatePeriod;

    /**
     * Set `secret.rotate.period` in terms of minute
     *
     * @param period
     *         the minutes between two secret rotate happening
     * @return this config object
     * @see AppConfigKey#SECRET_ROTATE_PERIOD
     */
    protected T secretRotatePeroid(int period) {
        E.illegalArgumentIf(period < 1, "minimum secret.rotate.period is 1 (minute)");
        secretRotatePeriod = period;
        return me();
    }

    public int secretRotatePeriod() {
        if (null == secretRotatePeriod) {
            boolean validSetting = true;
            String s = get(SECRET_ROTATE_PERIOD, "30");
            int minutes;
            if (!N.isInt(s)) {
                int seconds = Time.parseDuration(s);
                int reminder = seconds % 60;
                if (0 != reminder) {
                    validSetting = false;
                    seconds += (60 - reminder);
                }
                minutes = seconds / 60;
            } else {
                minutes = Integer.parseInt(s);
            }
            if (minutes <= 0) {
                validSetting = false;
                minutes = 30;
            }
            secretRotatePeriod = RotationSecretProvider.roundToPeriod(minutes);
            validSetting = validSetting && (secretRotatePeriod == minutes);
            if (!validSetting) {
                warn("invalid secret.rotate.period setting found: %s, system automatically set it to: %s", s, secretRotatePeriod);
            }
        }
        return secretRotatePeriod;
    }

    private void _mergeSecretRotatePeriod(AppConfig config) {
        if (!hasConfiguration(SECRET_ROTATE_PERIOD)) {
            secretRotatePeriod = config.secretRotatePeriod;
        }
    }

    private volatile SecureTicketCodec secureTicketCodec;
    private String secureTicketCodecClass;

    protected T secureTicketCodec(String secureTicketCodecClass) {
        this.secureTicketCodecClass = $.requireNotNull(secureTicketCodecClass);
        return me();
    }

    protected T secureTicketCodec(SecureTicketCodec codec) {
        this.secureTicketCodec = $.requireNotNull(codec);
        return me();
    }

    public SecureTicketCodec secureTicketCodec() {
        if (null != secureTicketCodec) {
            return secureTicketCodec;
        }
        synchronized (this) {
            if (null != secureTicketCodec) {
                return secureTicketCodec;
            }
            if (null == secureTicketCodecClass) {
                secureTicketCodecClass = get(SECURE_TICKET_CODEC, null);
                if (null == secureTicketCodecClass) {
                    secureTicketCodec = app().getInstance(DefaultSecureTicketCodec.class);
                    return secureTicketCodec;
                }
                if ("username".equalsIgnoreCase(secureTicketCodecClass)) {
                    secureTicketCodec = app().getInstance(UsernameSecureTicketCodec.class);
                    return secureTicketCodec;
                }
                secureTicketCodec = app().getInstance(secureTicketCodecClass);
            }
        }
        return secureTicketCodec;
    }

    private void _mergeSecureTicketCodec(AppConfig config) {
        if (!hasConfiguration(AppConfigKey.SECURE_TICKET_CODEC)) {
            secureTicketCodec = config.secureTicketCodec;
            secureTicketCodecClass = config.secureTicketCodecClass;
        }
    }

    private Boolean traceHandler;

    protected T traceHandler(boolean enabled) {
        this.traceHandler = enabled;
        return me();
    }

    public boolean traceHandler() {
        if (null == traceHandler) {
            traceHandler = get(TRACE_HANDLER_ENABLED, false);
        }
        return traceHandler;
    }

    // must be public, otherwise it get IllegalAccessError
    public void toggleTraceHandler(boolean enabled) {
        traceHandler = enabled;
    }

    private void _mergeTraceHandler(AppConfig config) {
        if (!hasConfiguration(TRACE_HANDLER_ENABLED)) {
            this.traceHandler = config.traceHandler;
        }
    }

    private Boolean traceRequest;

    protected T traceRequests(boolean enabled) {
        this.traceRequest = enabled;
        return me();
    }

    public boolean traceRequests() {
        if (null == traceRequest) {
            traceRequest = get(TRACE_REQUEST_ENABLED, false);
        }
        return traceRequest;
    }

    // must be public, otherwise it get IllegalAccessError
    public void toggleTraceRequest(boolean enabled) {
        traceRequest = enabled;
    }

    private void _mergeTraceRequests(AppConfig config) {
        if (!hasConfiguration(TRACE_REQUEST_ENABLED)) {
            this.traceRequest = config.traceRequest;
        }
    }

    private List<File> moduleBases;

    public List<File> moduleBases() {
        if (null == moduleBases) {
            String v = get(AppConfigKey.MODULES, null);
            moduleBases = processModules(v);
        }
        return moduleBases;
    }

    private List<File> processModules(String v) {
        if (S.blank(v)) {
            return C.list();
        } else {
            List<File> files = new ArrayList<>();
            File base = app.base();
            for (String s : v.trim().split("[;:]+")) {
                s = s.trim();
                File file;
                if (s.startsWith("/") || s.startsWith("\\")) {
                    file = new File(s);
                } else {
                    file = ProjectLayout.Utils.file(base, s);
                }
                if (!file.isDirectory()) {
                    logger.warn("Cannot locate extra source dir: %s", s);
                } else {
                    files.add(file);
                }
            }
            return C.list(files);
        }
    }

    private Boolean metricEnabled;

    protected T metricEnable(boolean enable) {
        this.metricEnabled = enable;
        return me();
    }

    public boolean metricEnabled() {
        if (null == metricEnabled) {
            metricEnabled = get(METRIC_ENABLED, true);
        }
        return metricEnabled;
    }

    private void _mergeMetricEnabled(AppConfig conf) {
        if (!hasConfiguration(METRIC_ENABLED)) {
            metricEnabled = conf.metricEnabled;
        }
    }

    public boolean possibleControllerClass(String className) {
        return appClassTester().test(className);
    }

    private CacheServiceProvider cacheServiceProvider = null;

    protected T cacheService(CacheServiceProvider provider) {
        E.NPE(provider);
        this.cacheServiceProvider = provider;
        return me();
    }

    protected T cacheService(Class<? extends CacheServiceProvider> provider) {
        this.cacheServiceProvider = $.newInstance(provider);
        return me();
    }

    public CacheService cacheService(String name) {
        CacheService cacheService = cacheServiceProvider().get(name);
        E.illegalStateIf(cacheService.state().isShutdown(), "Cache service[%s] already shutdown.", name);
        if (!cacheService.state().isStarted()) {
            cacheService.startup();
        }
        return cacheService;
    }

    public CacheServiceProvider cacheServiceProvider() {
        if (null == cacheServiceProvider) {
            CacheServiceProvider.Impl.setClassLoader(app().classLoader());
            try {
                cacheServiceProvider = get(AppConfigKey.CACHE_IMPL, null);
            } catch (ConfigurationException e) {
                Object obj = helper.getValFromAliases(raw, AppConfigKey.CACHE_IMPL.toString(), "impl", null);
                cacheServiceProvider = CacheServiceProvider.Impl.valueOfIgnoreCase(obj.toString());
                if (null != cacheServiceProvider) {
                    set(AppConfigKey.CACHE_IMPL, cacheServiceProvider);
                } else {
                    throw e;
                }
            }
            if (null == cacheServiceProvider) {
                cacheServiceProvider = CacheServiceProvider.Impl.Auto;
            }
        }
        return cacheServiceProvider;
    }

    public void resetCacheServices() {
        if (!Act.isDev()) {
            return;
        }
        OsglConfig.internalCache().clear();

    }

    private void _mergeCacheServiceProvider(AppConfig config) {
        if (!hasConfiguration(AppConfigKey.CACHE_IMPL)) {
            cacheServiceProvider = config.cacheServiceProvider;
        }
    }

    private String _cacheName;

    protected T cacheName(String name) {
        this._cacheName = name;
        return me();
    }

    public String cacheName() {
        if (null == _cacheName) {
            _cacheName = get(AppConfigKey.CACHE_NAME, "_act_app_");
        }
        return _cacheName;
    }

    private void _mergeCacheName(AppConfig config) {
        if (!hasConfiguration(AppConfigKey.CACHE_NAME)) {
            _cacheName = config._cacheName;
        }
    }

    private String _cacheNameSession;

    protected T cacheNameSession(String name) {
        this._cacheNameSession = name;
        return me();
    }

    public String cacheNameSession() {
        if (null == _cacheNameSession) {
            _cacheNameSession = get(AppConfigKey.CACHE_NAME_SESSION, cacheName());
        }
        return _cacheNameSession;
    }

    private void _mergeCacheNameSession(AppConfig config) {
        if (!hasConfiguration(CACHE_NAME_SESSION)) {
            _cacheNameSession = config._cacheNameSession;
        }
    }

    private Integer cacheTtl;

    protected T cacheTtl(int ttl) {
        cacheTtl = ttl;
        return me();
    }

    public int cacheTtl() {
        if (null == cacheTtl) {
            cacheTtl = getInteger(CACHE_TTL, 60);
        }
        return cacheTtl;
    }

    private void _mergeCacheTtl(AppConfig config) {
        if (!hasConfiguration(CACHE_TTL)) {
            cacheTtl = config.cacheTtl;
        }
    }

    private UnknownHttpMethodProcessor _unknownHttpMethodProcessor = null;

    protected T unknownHttpMethodProcessor(UnknownHttpMethodProcessor handler) {
        this._unknownHttpMethodProcessor = $.requireNotNull(handler);
        return me();
    }

    public UnknownHttpMethodProcessor unknownHttpMethodProcessor() {
        if (null == _unknownHttpMethodProcessor) {
            _unknownHttpMethodProcessor = get(AppConfigKey.HANDLER_UNKNOWN_HTTP_METHOD, UnknownHttpMethodProcessor.METHOD_NOT_ALLOWED);
        }
        return _unknownHttpMethodProcessor;
    }

    private void _mergeUnknownHttpMethodHandler(AppConfig config) {
        if (!hasConfiguration(AppConfigKey.HANDLER_UNKNOWN_HTTP_METHOD)) {
            this._unknownHttpMethodProcessor = config._unknownHttpMethodProcessor;
        }
    }

    private Integer resourcePreloadSizeLimit;

    protected T resourcePreloadSizeLimit(int limit) {
        resourcePreloadSizeLimit = limit;
        return me();
    }

    public int resourcePreloadSizeLimit() {
        if (null == resourcePreloadSizeLimit) {
            resourcePreloadSizeLimit = get(RESOURCE_PRELOAD_SIZE_LIMIT, 1024 * 10);
            if (resourcePreloadSizeLimit <= 0) {
                logger.warn("resource.preload.size.limit is set to zero or below, resource preload is disabled!");
            }
        }
        return resourcePreloadSizeLimit;
    }

    private void _mergeResourcePreloadSizeLimit(AppConfig conf) {
        if (!hasConfiguration(RESOURCE_PRELOAD_SIZE_LIMIT)) {
            this.resourcePreloadSizeLimit = conf.resourcePreloadSizeLimit;
        }
    }

    private Boolean resourceFiltering;

    protected T resourceFiltering(boolean b) {
        resourceFiltering = b;
        return me();
    }

    public boolean resourceFiltering() {
        if (null == resourceFiltering) {
            if (app.isDev()) {
                resourceFiltering = get(RESOURCE_FILTERING, true);
            } else {
                resourceFiltering = false;
            }
        }
        return resourceFiltering;
    }

    private void _mergeResourceFiltering(AppConfig conf) {
        if (!hasConfiguration(RESOURCE_FILTERING)) {
            this.resourceFiltering = conf.resourceFiltering;
        }
    }

    private Integer uploadInMemoryCacheThreshold;

    protected T uploadInMemoryCacheThreshold(int l) {
        uploadInMemoryCacheThreshold = l;
        return me();
    }

    public int uploadInMemoryCacheThreshold() {
        if (null == uploadInMemoryCacheThreshold) {
            uploadInMemoryCacheThreshold = get(UPLOAD_IN_MEMORY_CACHE_THRESHOLD, 1024 * 10);
        }
        return uploadInMemoryCacheThreshold;
    }

    private void _mergeUploadInMemoryCacheThreshold(AppConfig config) {
        if (!hasConfiguration(UPLOAD_IN_MEMORY_CACHE_THRESHOLD)) {
            uploadInMemoryCacheThreshold = config.uploadInMemoryCacheThreshold;
        }
    }

    private Boolean ssl;

    protected T supportSsl(boolean b) {
        ssl = b;
        return me();
    }

    public boolean supportSsl() {
        if (null == ssl) {
            ssl = get(SSL, false);
        }
        return ssl;
    }

    private void _mergeSslSupport(AppConfig config) {
        if (!hasConfiguration(SSL)) {
            ssl = config.ssl;
        }
    }

    private String wsTicketKey;

    protected T wsTicketeKey(String wsTicketKey) {
        this.wsTicketKey = wsTicketKey;
        return me();
    }

    public String wsTicketKey() {
        if (null == wsTicketKey) {
            wsTicketKey = get(WS_KEY_TICKET, "ws_ticket");
        }
        return wsTicketKey;
    }

    private void _mergeWsTicketKey(AppConfig config) {
        if (!hasConfiguration(WS_KEY_TICKET)) {
            wsTicketKey = config.wsTicketKey;
        }
    }

    private Integer wsPurgeClosedConnPeriod;

    protected T wsPurgeClosedConnPeriod(int period) {
        this.wsPurgeClosedConnPeriod = period;
        return me();
    }

    public int wsPurgeClosedConnPeriod() {
        if (null == wsPurgeClosedConnPeriod) {
            wsPurgeClosedConnPeriod = get(WS_PURGE_CLOSED_CONN_PERIOD, Act.isDev() ? 1 : 10);
        }
        return wsPurgeClosedConnPeriod;
    }

    private void _mergeWsPurgeClosedConnPeroid(AppConfig config) {
        if (!hasConfiguration(WS_PURGE_CLOSED_CONN_PERIOD)) {
            wsPurgeClosedConnPeriod = config.wsPurgeClosedConnPeriod;
        }
    }

    private Set<AppConfigurator> mergeTracker = C.newSet();

    public void loadJarProperties(Map<String, Properties> jarProperties) {
        // load common properties
        Properties p0 = jarProperties.remove("common");
        // load app env properties
        Properties p1 = jarProperties.remove(ConfLoader.confSetName());
        if (null != p1) {
            if (null != p0) {
                p0.putAll(p1);
            } else {
                p0 = p1;
            }
        }
        if (null != p0) {
            loadJarProperties(p0);
        }
    }

    private void loadJarProperties(Properties p) {
        Enumeration<?> keys = p.propertyNames();
        while (keys.hasMoreElements()) {
            String key = S.string(keys.nextElement());
            if (!raw.containsKey(key)) {
                raw.put(key, p.getProperty(key));
            }
        }
    }


    /**
     * Merge application configurator settings. Note application configurator
     * settings has lower priority as it's hardcoded thus only when configuration file
     * does not provided the settings, the app configurator will take effect
     *
     * @param conf
     *         the application configurator
     */
    public void _merge(AppConfigurator conf) {
        app.emit(SysEventId.CONFIG_PREMERGE);
        if (mergeTracker.contains(conf)) {
            return;
        }
        mergeTracker.add(conf);
        for (Method method : AppConfig.class.getDeclaredMethods()) {
            boolean isPrivate = Modifier.isPrivate(method.getModifiers());
            if (isPrivate && method.getName().startsWith("_merge")) {
                method.setAccessible(true);
                $.invokeVirtual(this, method, conf);
            }
        }


        Set<String> keys = conf.propKeys();
        if (!keys.isEmpty()) {
            for (String k : keys) {
                if (!raw.containsKey(k)) {
                    raw.put(k, conf.propVal(k));
                }
            }
        }
    }

    @Override
    protected void releaseResources() {
        app = null;
        super.releaseResources();
    }

    protected T me() {
        return $.cast(this);
    }

    private String cookieName(String suffix) {
        return S.concat(cookiePrefix(), suffix);
    }


    private ConcurrentMap<$.T2<Locale, DateTimeType>, String> localizedDateTimePatterns0 = new ConcurrentHashMap<>();

    private String getLocalizedDateTimePattern(Locale locale, DateTimeType dateTimeType) {
        $.T2<Locale, DateTimeType> key = $.T2(locale, dateTimeType);
        String s = localizedDateTimePatterns0.get(key);
        if (null != s) {
            return s;
        }
        String localStr = locale.toString().toLowerCase();
        String conf = S.concat("fmt.", localStr, dateTimeType.suffix());
        s = get(conf);
        if (null == s) {
            boolean canSplit = localStr.contains("_");
            if (canSplit) {
                s = get(S.concat("fmt.", S.replace('_').with('-').in(localStr), dateTimeType.suffix()));
                if (null == s) {
                    s = get(S.concat("fmt.", S.cut(localStr).after("_"), dateTimeType.suffix()));
                    if (null == s) {
                        s = get(S.concat("fmt.", S.cut(localStr).before("_"), dateTimeType.suffix()));
                    }
                }
            }
        }
        if (null == s) {
            if (isLocaleMatchesDefault(locale)) {
                s = dateTimeType.defaultPattern(this);
            } else {
                DateTimeStyle style = dateTimeStyle();
                if (dateTimeType == DateTimeType.DATE) {
                    style = dateStyle();
                } else if (dateTimeType == DateTimeType.TIME) {
                    style = timeStyle();
                }
                s = dateTimeType.defaultPattern(style, locale);
            }
        } else {
            if (S.eq("long", s, S.IGNORECASE)) {
                s = dateTimeType.defaultPattern(DateTimeStyle.LONG, locale);
            } else if (S.eq("medium", s, S.IGNORECASE)) {
                s = dateTimeType.defaultPattern(DateTimeStyle.MEDIUM, locale);
            } else if (S.eq("short", s, S.IGNORECASE)) {
                s = dateTimeType.defaultPattern(DateTimeStyle.SHORT, locale);
            }
        }
        localizedDateTimePatterns0.putIfAbsent(key, s);
        return s;
    }


    private volatile DateTimeStyle dateTimeStyle;

    public DateTimeStyle dateTimeStyle() {
        if (null == dateTimeStyle) {
            synchronized (this) {
                if (null == dateTimeStyle) {
                    dateTimePattern();
                }
            }
        }
        return dateTimeStyle;
    }

    private volatile DateTimeStyle dateStyle;

    public DateTimeStyle dateStyle() {
        if (null == dateStyle) {
            synchronized (this) {
                if (null == dateStyle) {
                    datePattern();
                }
            }
        }
        return dateStyle;
    }

    private volatile DateTimeStyle timeStyle;

    public DateTimeStyle timeStyle() {
        if (null == timeStyle) {
            synchronized (this) {
                if (null == timeStyle) {
                    timePattern();
                }
            }
        }
        return timeStyle;
    }

    public void setDefaultTldReloadCron() {
        raw.put(canonical(TopLevelDomainList.CRON_TLD_RELOAD), "0 0 2 * * *");
    }
}
