/*
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
 *
 * Copyright (c) 2013-2014 sagyf Yang. The Four Group.
 */

package goja;

import com.google.common.base.MoreObjects;
import com.google.common.base.Strings;
import com.google.common.collect.Lists;
import com.google.common.io.Resources;
import com.google.common.primitives.Ints;

import com.jfinal.config.Constants;
import com.jfinal.config.Handlers;
import com.jfinal.config.Interceptors;
import com.jfinal.config.JFinalConfig;
import com.jfinal.config.Plugins;
import com.jfinal.config.Routes;
import com.jfinal.core.Const;
import com.jfinal.ext.handler.ContextPathHandler;
import com.jfinal.json.FastJsonFactory;
import com.jfinal.json.JacksonFactory;
import com.jfinal.kit.PropKit;
import com.jfinal.plugin.IPlugin;
import com.jfinal.plugin.druid.DruidStatViewHandler;
import com.jfinal.plugin.druid.IDruidStatViewAuth;
import com.jfinal.plugin.ehcache.EhCachePlugin;
import com.jfinal.plugin.redis.RedisPlugin;
import com.jfinal.render.FreeMarkerRender;
import com.jfinal.render.ViewType;
import com.jfinal.template.Engine;
import com.jfinal.upload.OreillyCos;
import com.oreilly.servlet.multipart.DefaultFileRenamePolicy;

import freemarker.template.Configuration;

import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.io.FileUtils;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.LoggerFactory;

import java.io.File;
import java.net.URL;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;

import goja.core.Func;
import goja.core.app.ApplicationMode;
import goja.core.app.GojaConfig;
import goja.core.app.GojaPropConst;
import goja.core.exceptions.GojaException;
import goja.core.exceptions.UnexpectedException;
import goja.core.kits.reflect.ClassPathScanning;
import goja.core.sqlinxml.SqlInXmlPlugin;
import goja.initialize.DruidDbIntializer;
import goja.initialize.GojaInitializer;
import goja.job.Job;
import goja.job.JobsPlugin;
import goja.logging.Logger;
import goja.logging.LoggerInit;
import goja.mvc.PageViewKit;
import goja.mvc.controller.ControllerBindRoutes;
import goja.mvc.interceptor.AutoInterceptor;
import goja.mvc.render.ftl.PrettyTimeDirective;
import goja.mvc.render.ftl.layout.BlockDirective;
import goja.mvc.render.ftl.layout.ExtendsDirective;
import goja.mvc.render.ftl.layout.OverrideDirective;
import goja.mvc.render.ftl.layout.SuperDirective;
import goja.mvc.render.ftl.shiro.ShiroTags;
import goja.plugins.shiro.ShiroInterceptor;
import goja.plugins.shiro.ShiroPlugin;
import goja.rapid.job.QuartzPlugin;
import goja.rapid.mongo.MongoPlugin;
import goja.mvc.handler.CutSessionIdHandler;
import goja.mvc.interceptor.syslog.LogProcessor;
import goja.mvc.interceptor.syslog.SysLogInterceptor;
import goja.mvc.upload.filerenamepolicy.DateRandomFileRenamePolicy;
import goja.mvc.upload.filerenamepolicy.RandomFileRenamePolicy;
import goja.security.shiro.SecurityUserData;
import redis.clients.jedis.Protocol;

//import goja.annotation.HandlerBind;

/**
 * <p> The core of goja. </p>
 *
 * @author sagyf yang
 * @version 1.0 2014-06-02 11:11
 * @since JDK 1.6
 */
public class Goja extends JFinalConfig {

    private static final org.slf4j.Logger logger = LoggerFactory.getLogger(Goja.class);
    // the application configuration.
    public static Properties configuration;
    // the application view path.
    public static String viewPath;
    public static String appName;

    public static SecurityUserData securityUserData;

    static boolean started = false;

    private static JobsPlugin jobsPlugin = new JobsPlugin();

    private Routes _routes;

    /**
     * 为方便测试用例的使用，这个提供一个手动初始化的方法为测试用例使用,调用采用反射机制 <p/> Reflect.on(Goja.class).call("initWithTest");
     */
    @SuppressWarnings("unused")
    static void initWithTest() {

        // set config propertis.
        configuration = GojaConfig.getConfigProps();

        LoggerInit.init();
    }

    @Override
    public void configConstant(Constants constants) {
        // set config propertis.
        configuration = GojaConfig.getConfigProps();

        // dev_mode
        final ApplicationMode applicationMode = GojaConfig.getApplicationMode();
        final boolean isDev = applicationMode.isDev();
        constants.setDevMode(isDev);
        if (applicationMode.isProd()) {
            // 404由于在开发模式下的提示信息，由于404触发的，所以在开发模式不启动404视图界面
            constants.setError404View(PageViewKit.get404PageView());
        }
        constants.setError500View(PageViewKit.get500PageView());
        constants.setError403View(PageViewKit.get403PageView());

        appName = GojaConfig.getAppName();

        if (GojaConfig.isSecurity()) {
            final Set<Class<? extends SecurityUserData>> securityUserClass = ClassPathScanning.scan(SecurityUserData.class);
            if (CollectionUtils.isNotEmpty(securityUserClass)) {
                for (Class<? extends SecurityUserData> securityUserClas : securityUserClass) {
                    try {
                        securityUserData = securityUserClas.newInstance();
                        break;
                    } catch (InstantiationException | IllegalAccessException e) {
                        logger.error("the security user data has error!", e);
                    }
                }
            }
        }

        final boolean jspViewType = GojaConfig.getPropertyToBoolean(GojaPropConst.APP_VIEW_JSP, false);
        if (jspViewType) {
            constants.setViewType(ViewType.JSP);
        } else {
            setFtlSharedVariable();
        }
        final int uploadMaxFileSize = GojaConfig.getPropertyToInt(GojaPropConst.APP_UPLOAD_MAXFILESIZE, Const.DEFAULT_MAX_POST_SIZE);
        constants.setMaxPostSize(uploadMaxFileSize);

        final String attachmentPath = GojaConfig.getProperty(GojaPropConst.APP_UPLOAD_PATH, "attachment");
        constants.setBaseUploadPath(attachmentPath);
        constants.setBaseDownloadPath(attachmentPath);

        final String jsonMode = GojaConfig.getJsonMode();
        if (!Strings.isNullOrEmpty(jsonMode)) {
            if (StringUtils.equalsIgnoreCase("fastjson", jsonMode)) {
                constants.setJsonFactory(new FastJsonFactory());
            } else if (StringUtils.equalsIgnoreCase("jackson", jsonMode)) {
                constants.setJsonFactory(new JacksonFactory());
            }
        }

        final String fileRenamePolicy = GojaConfig.getProperty(GojaPropConst.APP_UPLOAD_FILERENAMEPOLICY, "default");
        if (StringUtils.equalsIgnoreCase(fileRenamePolicy, "date")) {
            OreillyCos.setFileRenamePolicy(new DateRandomFileRenamePolicy());
        } else if (StringUtils.equalsIgnoreCase(fileRenamePolicy, "radom")) {
            OreillyCos.setFileRenamePolicy(new RandomFileRenamePolicy());
        } else if (StringUtils.equalsIgnoreCase(fileRenamePolicy, "default")) {
            OreillyCos.setFileRenamePolicy(new DefaultFileRenamePolicy());
        } else {
            logger.warn("Upload folder naming of the unknown!");
        }
    }

    @Override
    public void configRoute(Routes routes) {
        this._routes = routes;

        // fixed: render view has views//xxx.ftl
        final String DEFAULT_VIEW_PATH = PageViewKit.WEBINF_DIR + "views";
        viewPath = GojaConfig.getProperty(GojaPropConst.APP_VIEWPATH, DEFAULT_VIEW_PATH);
        routes.setBaseViewPath(viewPath);

        routes.add(new ControllerBindRoutes());
    }

    @Override
    public void configEngine(Engine me) {
        me.setDevMode(GojaConfig.getApplicationMode().isDev());
        me.setEncoding("utf-8");
    }

    @Override
    public void configPlugin(Plugins plugins) {
        plugins.add(jobsPlugin);
        // fixed: https://github.com/GojaFramework/goja/issues/4
        started = true;

        initDataSource(plugins);

        // 判断初始化缓存信息
        plugins.add(new EhCachePlugin());

        if (GojaConfig.isSecurity()) {
            plugins.add(new ShiroPlugin(this._routes));
        }

        if (GojaConfig.getPropertyToBoolean(GojaPropConst.APPJOB, false)) {
            plugins.add(new QuartzPlugin());
        }

        final boolean mongoFlag = GojaConfig.getPropertyToBoolean(GojaPropConst.MONGO, false);
        if (mongoFlag) {
            logger.info("开始初始化MongoDB插件");

            final String mongoHost = GojaConfig.getProperty(GojaPropConst.MONGO_HOST);
            final String mongoPort = GojaConfig.getProperty(GojaPropConst.MONGO_PORT);
            final String mongoDatabase = GojaConfig.getProperty(GojaPropConst.MONGO_DB, "test");
            if (Strings.isNullOrEmpty(mongoHost) && Strings.isNullOrEmpty(mongoPort)) {
                plugins.add(new MongoPlugin(mongoDatabase));
            } else {
                plugins.add(new MongoPlugin(mongoHost,
                        MoreObjects.firstNonNull(Ints.tryParse(mongoPort), MongoPlugin.DEFAUL_PORT),
                        mongoDatabase));
            }
        }

        final String redisConfig = GojaConfig.getProperty(GojaPropConst.REDIS_CONFIG);
        if (!Strings.isNullOrEmpty(redisConfig)) {
            final Properties redisConfigProp;
            final File configFolderFile = GojaConfig.getConfigFolderFile();
            redisConfigProp = configFolderFile == null ? PropKit.use(redisConfig).getProperties()
                    : PropKit.use(FileUtils.getFile(configFolderFile, redisConfig)).getProperties();
            String cacheNames = redisConfigProp.getProperty(GojaPropConst.REDIS_CACHES);
            if (!Strings.isNullOrEmpty(cacheNames)) {
                List<String> cacheNameList = Func.COMMA_SPLITTER.splitToList(cacheNames);
                for (String cacheName : cacheNameList) {
                    final String cacheRedistPort = redisConfigProp.getProperty(cacheName + ".port");
                    final String cacheRedistHost = redisConfigProp.getProperty(cacheName + ".host",
                            String.valueOf(Protocol.DEFAULT_PORT));
                    int port = Strings.isNullOrEmpty(cacheRedistPort) ? Protocol.DEFAULT_PORT
                            : MoreObjects.firstNonNull(Ints.tryParse(cacheRedistPort), Protocol.DEFAULT_PORT);
                    final RedisPlugin jedis = new RedisPlugin(cacheName, cacheRedistHost, port);
                    plugins.add(jedis);
                }
            }
        } else {
            final String redis_host = GojaConfig.getProperty(GojaPropConst.REDIS_HOST, StringUtils.EMPTY);
            if (!Strings.isNullOrEmpty(redis_host)) {
                final String cacheName =
                        GojaConfig.getProperty(GojaPropConst.REDIS_CACHENAME, "goja.redis.cache");
                final String strProt = GojaConfig.getProperty(GojaPropConst.REDIS_PORT);
                int port = Strings.isNullOrEmpty(strProt) ? Protocol.DEFAULT_PORT
                        : MoreObjects.firstNonNull(Ints.tryParse(strProt), Protocol.DEFAULT_PORT);
                final RedisPlugin jedis = new RedisPlugin(cacheName, redis_host, port);
                plugins.add(jedis);
            }
        }

        final Set<Class<? extends IPlugin>> pluginClasses = ClassPathScanning.scan(IPlugin.class);
        if (CollectionUtils.isNotEmpty(pluginClasses)) {

            for (Class<? extends IPlugin> pluginClass : pluginClasses) {
                try {
                    plugins.add(pluginClass.newInstance());
                } catch (InstantiationException | IllegalAccessException e) {
                    Logger.error("The plugin instance is error!", e);
                }
            }
        }

    }

    @Override
    public void configInterceptor(Interceptors interceptors) {
        try {

            final Set<Class<? extends LogProcessor>> logProcessors = ClassPathScanning.scan(LogProcessor.class);
            if (CollectionUtils.isNotEmpty(logProcessors)) {
                final Class<? extends LogProcessor> logProcessClass = Lists.newArrayList(logProcessors).get(0);

                final URL config_url = Resources.getResource("syslog.json");
                if (config_url != null) {
                    SysLogInterceptor sysLogInterceptor = new SysLogInterceptor();
                    sysLogInterceptor = sysLogInterceptor.setLogProcesser(logProcessClass.newInstance());
                    if (sysLogInterceptor != null) {
                        interceptors.add(sysLogInterceptor);
                    }
                }
            }
        } catch (IllegalArgumentException | IllegalAccessException | InstantiationException e) {
            logger.error("Enable the system operation log interceptor abnormalities.", e);
        }

        AutoInterceptor.setAppInterceptor(interceptors);
        if (GojaConfig.isSecurity()) {
            interceptors.add(new ShiroInterceptor());
        }
    }

    @Override
    public void configHandler(Handlers handlers) {

        handlers.add(new ContextPathHandler("ctx"));
        final boolean monitorDB = GojaConfig.getPropertyToBoolean(GojaPropConst.DB_MONITOR, false);
        if (monitorDB) {
            final String view_url =
                    GojaConfig.getProperty(GojaPropConst.DB_MONITOR_URL, "/druid/monitor");

            final DruidStatViewHandler dvh = new DruidStatViewHandler(view_url, new IDruidStatViewAuth() {
                @Override
                public boolean isPermitted(HttpServletRequest request) {
                    HttpSession hs = request.getSession(false);
                    return (hs != null);
                }
            });

            handlers.add(dvh);
        }

        if (GojaConfig.isSecurity()) {
            handlers.add(new CutSessionIdHandler());
        }

        //        final List<Class> handler_clses = ClassBox.getInstance().getClasses(ClassType.HANDLER);
        //        if (handler_clses != null && !handler_clses.isEmpty()) {
        //            HandlerBind handlerBind;
        //            for (Class handler : handler_clses) {
        //                handlerBind = (HandlerBind) handler.getAnnotation(HandlerBind.class);
        //                if (handlerBind != null) {
        //                    try {
        //                        handlers.add((com.jfinal.handler.Handler) handler.newInstance());
        //                    } catch (InstantiationException e) {
        //                        logger.error("The Handler instance is error!", e);
        //                    } catch (IllegalAccessException e) {
        //                        logger.error("The Handler instance is error!", e);
        //                    }
        //                }
        //            }
        //        }
    }

    @Override
    public void afterJFinalStart() {

        final List<Job<?>> applicationStartJobs = jobsPlugin.getApplicationStartJobs();
        if (applicationStartJobs != null && !applicationStartJobs.isEmpty()) {
            for (Job<?> applicationStartJob : applicationStartJobs) {

                applicationStartJob.run();
                if (applicationStartJob.isWasError()) {
                    if (applicationStartJob.getLastException() != null) {
                        logger.error("@OnApplicationStart Job has failed!", applicationStartJob.getLastException());
                    } else {
                        logger.error("@OnApplicationStart Job has failed");
                    }
                }
            }

        }

        GojaInitializer.finish();
    }

    @Override
    public void beforeJFinalStop() {
        started = false;

        final List<Class> stopJobs = jobsPlugin.getApplicationStopJobs();

        if (!(stopJobs == null || stopJobs.isEmpty())) {
            for (Class clazz : stopJobs) {
                try {
                    Job<?> job = ((Job<?>) clazz.newInstance());
                    job.run();

                    if (job.isWasError()) {
                        if (job.getLastException() != null) {
                            logger.error("@OnApplicationStop Job has failed!", job.getLastException());
                        } else {
                            logger.error("@OnApplicationStop Job has failed");
                        }
                    }
                } catch (InstantiationException | IllegalAccessException e) {
                    throw new UnexpectedException("ApplicationStop job could not be instantiated", e);
                } catch (Throwable ex) {
                    if (ex instanceof GojaException) {
                        throw (GojaException) ex;
                    }
                    throw new UnexpectedException(ex);
                }
            }
        }
        jobsPlugin.clear();
        jobsPlugin = null;
        _routes.clear();
        _routes = null;

    }

    /**
     * init databases.
     *
     * @param plugins plugin.
     */
    private void initDataSource(final Plugins plugins) {

        final Map<String, Properties> dbConfig = GojaConfig.loadDBConfig(GojaConfig.getConfigProps());
        for (String db_config : dbConfig.keySet()) {
            final Properties db_props = dbConfig.get(db_config);
            if (db_props != null && !db_props.isEmpty()) {
                DruidDbIntializer.init(db_config, plugins, db_props);
            }
        }

        if (GojaConfig.getPropertyToBoolean(GojaPropConst.DB_SQLINXML, true)) {
            plugins.add(new SqlInXmlPlugin());
        }
    }

    /**
     * set freemarker variable.
     */
    private void setFtlSharedVariable() {
        // custmer variable
        final Configuration config = FreeMarkerRender.getConfiguration();
        config.setSharedVariable("block", new BlockDirective());
        config.setSharedVariable("extends", new ExtendsDirective());
        config.setSharedVariable("override", new OverrideDirective());
        config.setSharedVariable("super", new SuperDirective());
        // 增加日期美化指令（类似 几分钟前）
        config.setSharedVariable("prettytime", new PrettyTimeDirective());
        if (GojaConfig.isSecurity()) {
            config.setSharedVariable("shiro", new ShiroTags(config.getObjectWrapper()));
        }
    }
}


package goja.mvc.datatables;

import com.google.common.base.Preconditions;
import com.google.common.base.Strings;
import com.google.common.collect.Lists;
import com.google.common.primitives.Ints;

import com.jfinal.plugin.activerecord.Model;
import com.jfinal.plugin.activerecord.Page;
import com.jfinal.plugin.activerecord.Record;

import org.apache.commons.lang3.BooleanUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.tuple.Pair;
import org.apache.commons.lang3.tuple.Triple;

import java.io.Serializable;
import java.util.Enumeration;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import javax.servlet.http.HttpServletRequest;

import goja.core.StringPool;
import goja.core.db.Condition;
import goja.core.kits.lang.Strs;

/**
 * <p> </p>
 *
 * @author sogYF
 * @version 1.0
 * @since JDK 1.6
 */
public final class DTCriterias implements Serializable {
    private static final long    serialVersionUID = -4728223524642774477L;
    /**
     * Matches the regular brackets
     */
    private static final Pattern BRACKETS_PATTERN = Pattern.compile("\\[(.*?)\\]");

    /**
     * jquery datatablse Control the query condition
     */
    private final DTSearch search;
    /**
     * Display start number
     */
    private final int            start;
    /**
     * The page displays the number of.
     */
    private final int            length;
    /**
     * Which coloumns to display.
     */
    private final List<DTColumn> columns;
    /**
     * The order coloumn.
     */
    private final List<DTOrder>  order;
    private final int            draw;

    private final List<Triple<String, Condition, Object>> params;

    private DTCriterias(DTSearch search, int start, int length, List<DTColumn> columns,
                        List<DTOrder> order, int draw) {
        this.search = search;
        this.start = start;
        this.length = length;
        this.columns = columns;
        this.order = order;
        this.draw = draw;
        this.params = Lists.newArrayListWithCapacity(1);
    }

    public static DTCriterias criteriasWithRequest(HttpServletRequest request) {
        if (request != null) {

            String r_draw = request.getParameter(DTConstants.DT_DRAW);
            String r_start = request.getParameter(DTConstants.DT_START);
            String r_length = request.getParameter(DTConstants.DT_LENGTH);
            String r_search_value = request.getParameter(DTConstants.DT_SEARCH_VALUE);
            String r_search_regex = request.getParameter(DTConstants.DT_SEARCH_REGEX);

            boolean custom_search =
                    !Strings.isNullOrEmpty(request.getParameter(DTConstants.DT_SEARCH_FLAG));

            int draw = Strings.isNullOrEmpty(r_draw) ? 1 : Ints.tryParse(r_draw);
            int start = Strings.isNullOrEmpty(r_start) ? 1 : Ints.tryParse(r_start);
            int length = Strings.isNullOrEmpty(r_length) ? 1 : Ints.tryParse(r_length);
            start = start == 0 ? 1 : start;

            DTSearch dtSearch = DTSearch.create(r_search_value, BooleanUtils.toBoolean(r_search_regex));

            final List<DTColumn> dtColumns = Lists.newArrayList();
            final List<DTOrder> dtOrders = Lists.newArrayList();

            List<Pair<Integer, String>> _orders = Lists.newArrayList();
            List<String> processed = Lists.newArrayList();

            List<Triple<String, Condition, Object>> _params = Lists.newArrayListWithCapacity(3);
            try {
                Matcher matcher;
                String p_index = null;
                final Enumeration<String> parameterNames = request.getParameterNames();
                while (parameterNames.hasMoreElements()) {
                    String param_name = parameterNames.nextElement();
                    if (custom_search && StringUtils.startsWith(param_name,
                            DTConstants.DT_CUSTEM_SEARCH_PREFIX)) {
                        final String req_val = request.getParameter(param_name);
                        if (!Strings.isNullOrEmpty(req_val)) {
                            String[] param_array = StringUtils.split(param_name, StringPool.DASH);
                            if (param_array != null && param_array.length >= 2) {

                                String name = param_array[1];
                                String condition =
                                        param_array.length == 2 ? Condition.EQ.toString() : param_array[2];
                                Condition query_condition;
                                if (Strings.isNullOrEmpty(condition)) {
                                    query_condition = Condition.EQ;
                                } else {
                                    query_condition = Condition.valueOf(condition.toUpperCase());
                                }
                                switch (query_condition) {
                                    case BETWEEN:
                                        String two_param =
                                                StringUtils.replace(param_name, Condition.BETWEEN.toString(), "AND");
                                        String req_val2 = request.getParameter(two_param);
                                        _params.add(Triple.<String, Condition, Object>of(name, query_condition,
                                                new String[]{req_val, req_val2}));
                                        break;
                                    case LIKE:
                                        _params.add(Triple.<String, Condition, Object>of(name, query_condition,
                                                Strs.like(req_val)));
                                        break;
                                    case LLIKE:
                                        _params.add(Triple.<String, Condition, Object>of(name, query_condition,
                                                Strs.llike(req_val)));
                                        break;
                                    case RLIKE:
                                        _params.add(Triple.<String, Condition, Object>of(name, query_condition,
                                                Strs.rlike(req_val)));
                                        break;
                                    default:
                                        _params.add(
                                                Triple.<String, Condition, Object>of(name, query_condition, req_val));
                                }
                            }
                        }
                    }

                    // column setting
                    matcher = BRACKETS_PATTERN.matcher(param_name);
                    if (matcher.find()) {
                        p_index = matcher.group(1);
                    }
                    if (StringUtils.startsWithIgnoreCase(param_name, DTConstants.DT_COLUMNS)) {

                        if (!processed.isEmpty() && processed.contains(p_index)) {
                            continue;
                        }
                        processed.add(p_index);
                        String column_data = request.getParameter("columns[" + p_index + "][data]");
                        if (Strings.isNullOrEmpty(column_data)) {
                            p_index = null;
                            continue;
                        }
                        String column_name = request.getParameter("columns[" + p_index + "][name]");
                        String column_searchable = request.getParameter("columns[" + p_index + "][searchable]");
                        String column_orderable = request.getParameter("columns[" + p_index + "][orderable]");
                        String column_search_value =
                                request.getParameter("columns[" + p_index + "][search][value]");
                        String column_search_regex =
                                request.getParameter("columns[" + p_index + "][search][regex]");

                        dtColumns.add(new DTColumn(column_data, column_name
                                , BooleanUtils.toBoolean(column_searchable)
                                , BooleanUtils.toBoolean(column_orderable)
                                ,
                                DTSearch.create(column_search_value, BooleanUtils.toBoolean(column_search_regex))));
                    } else if (StringUtils.startsWithIgnoreCase(param_name, DTConstants.DT_ORDER)) {
                        int index = Ints.tryParse(p_index);
                        if (!_orders.isEmpty() && _orders.get(index) != null) {
                            continue;
                        }
                        String order_column_index = request.getParameter("order[" + p_index + "][column]");
                        String order_column_dir = request.getParameter("order[" + p_index + "][dir]");
                        Pair<Integer, String> _temp_order =
                                Pair.of(Ints.tryParse(order_column_index), order_column_dir);
                        _orders.add(_temp_order);
                    }
                }

                if (!_orders.isEmpty()) {
                    for (Pair<Integer, String> pair : _orders) {
                        DTColumn column = dtColumns.get(pair.getKey());
                        if (column == null) {
                            continue;
                        }
                        dtOrders.add(DTOrder.create(column.getData(), pair.getValue()));
                    }
                }
            } finally {
                _orders = null;
                processed = null;
            }

            final DTCriterias dtCriterias =
                    new DTCriterias(dtSearch, start, length, dtColumns, dtOrders, draw);
            dtCriterias.setAllParams(_params);
            return dtCriterias;
        } else {
            return null;
        }
    }

    public void setAllParams(List<Triple<String, Condition, Object>> _params) {
        this.params.addAll(_params);
    }

    /**
     * Adding custom query condition and value
     *
     * @param field     query condition
     * @param condition condition
     * @param value     query condition If it is a LIKE query, then you need to pass an array of objects,
     *                  the size of 2.
     */
    public void setParam(String field, Condition condition, Object value) {
        this.params.add(Triple.of(field, condition, value));
    }

    /**
     * Adding custom query equal  value.
     *
     * @param field query condition
     * @param value query condition
     */
    public void setParam(String field, Object value) {
        this.setParam(field, Condition.EQ, value);
    }

    public List<Triple<String, Condition, Object>> getParams() {
        return params;
    }

    /**
     * Support for a single entity with the integration of Datatables plugin。
     *
     * @param model The DB model.
     * @return response.
     */
    public DTResponse response(Class<? extends Model> model) {

        Preconditions.checkNotNull(this, "datatable criterias is must be not null.");
        final Page<Record> datas = DTDao.paginate(model, this);
        return DTResponse.build(this, datas.getList(), datas.getTotalRow(), datas.getTotalRow());
    }

    public DTResponse response(String whereSql, String columns, List<Object> params) {

        Preconditions.checkNotNull(this, "datatable criterias is must be not null.");
        final Page<Record> datas = DTDao.paginate(whereSql, columns, this, params);
        return DTResponse.build(this, datas.getList(), datas.getTotalRow(), datas.getTotalRow());
    }

    public DTSearch getSearch() {
        return search;
    }

    public int getStart() {
        return start;
    }

    public int getLength() {
        return length;
    }

    public List<DTColumn> getColumns() {
        return columns;
    }

    public List<DTOrder> getOrder() {
        return order;
    }

    public int getDraw() {
        return draw;
    }
}


/*
 * Copyright (C) 2011-2013 Eiichiro Uchiumi. All Rights Reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.eiichiro.bootleg;

import org.eiichiro.bootleg.json.JSONResponse;
import org.eiichiro.bootleg.xml.XMLResponse;

/**
 * An utility which has some factory methods to create {@link Response} instance
 * to be sent to the client form Web endpoint.
 *
 * @author Eiichiro Uchiumi
 */
public abstract class Responses {

	private Responses() {}

	/**
	 * Creates generic {@code Response} that has no message body.
	 *
	 * @return Generic {@code Response} that has no message body.
	 */
	public static Response response() {
		return new GenericResponse();
	}

	/**
	 * Creates generic {@code Response} that has the specified entity as message
	 * body.
	 *
	 * @param value The object to be sent to the client.
	 * @return Generic {@code Response} that has the specified entity as message
	 * body.
	 */
	public static Response response(Object value) {
		GenericResponse response = new GenericResponse();
		response.entity(value);
		return response;
	}

	/**
	 * Creates forward {@link Response} to forward the request to the specified
	 * path.
	 *
	 * @param path The path to forward.
	 * @return {@code Forward} to forward the request to the specified path.
	 */
	public static Forward forward(String path) {
		return new Forward(path);
	}

	/**
	 * Creates redirect {@link Response} to redirect the response to the
	 * specified URL.
	 *
	 * @param url The URL to redirect.
	 * @return {@code Redirect} to redirect the response to the specified URL.
	 */
	public static Redirect redirect(String url) {
		return new Redirect(url);
	}

	/**
	 * Creates {@code JSONResponse} to send the specified entity to the client
	 * as JSON format response.
	 *
	 * @param value The object to be sent to the client.
	 * @return {@code JSONResponse}.
	 */
	public static JSONResponse json(Object value) {
		JSONResponse response = new JSONResponse();
		response.entity(value);
		return response;
	}

	/**
	 * Creates {@code XMLResponse} to send the specified entity to the client as
	 * XML format response.
	 *
	 * @param value The object to be sent to the client.
	 * @return {@code XMLResponse}.
	 */
	public static XMLResponse xml(Object value) {
		XMLResponse response = new XMLResponse();
		response.entity(value);
		return response;
	}

}


/*
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
 *
 * Copyright (c) 2013-2014 sagyf Yang. The Four Group.
 */

package goja.core.kits;

import com.google.common.collect.Maps;

import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;
import java.util.Map;

/**
 * <p> Chinese Id Card Kit class. </p>
 *
 * @author sagyf yang
 * @version 1.0 2014-09-30 23:13
 * @since JDK 1.6
 */
@SuppressWarnings("UnusedDeclaration")
public abstract class IdCardKit {

    /**
     * 中国公民身份证号码最小长度。
     */
    public static final int CHINA_ID_MIN_LENGTH = 15;

    /**
     * 中国公民身份证号码最大长度。
     */
    public static final int CHINA_ID_MAX_LENGTH = 18;

    /**
     * 省、直辖市代码表
     */
    public static final String cityCode[] = {
            "11", "12", "13", "14", "15", "21", "22", "23", "31", "32", "33", "34", "35", "36", "37",
            "41",
            "42", "43", "44", "45", "46", "50", "51", "52", "53", "54", "61", "62", "63", "64", "65",
            "71",
            "81", "82", "91"
    };

    /**
     * 每位加权因子
     */
    public static final int power[] = {
            7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2
    };

    /**
     * 第18位校检码
     */
    public static final String verifyCode[] = {
            "1", "0", "X", "9", "8", "7", "6", "5", "4", "3", "2"
    };

    /**
     * 最低年限
     */
    public static final int MIN = 1930;

    public static final Map<String, String>  cityCodes   = Maps.newHashMap();
    /**
     * 台湾身份首字母对应数字
     */
    public static final Map<String, Integer> twFirstCode = Maps.newHashMap();
    /**
     * 香港身份首字母对应数字
     */
    public static final Map<String, Integer> hkFirstCode = Maps.newHashMap();

    static {
        cityCodes.put("11", "北京");
        cityCodes.put("12", "天津");
        cityCodes.put("13", "河北");
        cityCodes.put("14", "山西");
        cityCodes.put("15", "内蒙古");
        cityCodes.put("21", "辽宁");
        cityCodes.put("22", "吉林");
        cityCodes.put("23", "黑龙江");
        cityCodes.put("31", "上海");
        cityCodes.put("32", "江苏");
        cityCodes.put("33", "浙江");
        cityCodes.put("34", "安徽");
        cityCodes.put("35", "福建");
        cityCodes.put("36", "江西");
        cityCodes.put("37", "山东");
        cityCodes.put("41", "河南");
        cityCodes.put("42", "湖北");
        cityCodes.put("43", "湖南");
        cityCodes.put("44", "广东");
        cityCodes.put("45", "广西");
        cityCodes.put("46", "海南");
        cityCodes.put("50", "重庆");
        cityCodes.put("51", "四川");
        cityCodes.put("52", "贵州");
        cityCodes.put("53", "云南");
        cityCodes.put("54", "西藏");
        cityCodes.put("61", "陕西");
        cityCodes.put("62", "甘肃");
        cityCodes.put("63", "青海");
        cityCodes.put("64", "宁夏");
        cityCodes.put("65", "新疆");
        cityCodes.put("71", "台湾");
        cityCodes.put("81", "香港");
        cityCodes.put("82", "澳门");
        cityCodes.put("91", "国外");
        twFirstCode.put("A", 10);
        twFirstCode.put("B", 11);
        twFirstCode.put("C", 12);
        twFirstCode.put("D", 13);
        twFirstCode.put("E", 14);
        twFirstCode.put("F", 15);
        twFirstCode.put("G", 16);
        twFirstCode.put("H", 17);
        twFirstCode.put("J", 18);
        twFirstCode.put("K", 19);
        twFirstCode.put("L", 20);
        twFirstCode.put("M", 21);
        twFirstCode.put("N", 22);
        twFirstCode.put("P", 23);
        twFirstCode.put("Q", 24);
        twFirstCode.put("R", 25);
        twFirstCode.put("S", 26);
        twFirstCode.put("T", 27);
        twFirstCode.put("U", 28);
        twFirstCode.put("V", 29);
        twFirstCode.put("X", 30);
        twFirstCode.put("Y", 31);
        twFirstCode.put("W", 32);
        twFirstCode.put("Z", 33);
        twFirstCode.put("I", 34);
        twFirstCode.put("O", 35);
        hkFirstCode.put("A", 1);
        hkFirstCode.put("B", 2);
        hkFirstCode.put("C", 3);
        hkFirstCode.put("R", 18);
        hkFirstCode.put("U", 21);
        hkFirstCode.put("Z", 26);
        hkFirstCode.put("X", 24);
        hkFirstCode.put("W", 23);
        hkFirstCode.put("O", 15);
        hkFirstCode.put("N", 14);
    }

    /**
     * 将15位身份证号码转换为18位
     *
     * @param idCard 15位身份编码
     * @return 18位身份编码
     */
    public static String conver15CardTo18(String idCard) {
        String idCard18;
        if (idCard.length() != CHINA_ID_MIN_LENGTH) {
            return null;
        }
        if (isNum(idCard)) {
            // 获取出生年月日
            String birthday = idCard.substring(6, 12);
            Date birthDate = null;
            try {
                birthDate = new SimpleDateFormat("yyMMdd").parse(birthday);
            } catch (ParseException e) {
                e.printStackTrace();
            }
            Calendar cal = Calendar.getInstance();
            if (birthDate != null) {
                cal.setTime(birthDate);
            }
            // 获取出生年(完全表现形式,如：2010)
            String sYear = String.valueOf(cal.get(Calendar.YEAR));
            idCard18 = idCard.substring(0, 6) + sYear + idCard.substring(8);
            // 转换字符数组
            char[] cArr = idCard18.toCharArray();
            int[] iCard = converCharToInt(cArr);
            int iSum17 = getPowerSum(iCard);
            // 获取校验位
            String sVal = getCheckCode18(iSum17);
            if (sVal.length() > 0) {
                idCard18 += sVal;
            } else {
                return null;
            }
        } else {
            return null;
        }
        return idCard18;
    }

    /**
     * 验证身份证是否合法
     */
    public static boolean validateCard(String idCard) {
        String card = idCard.trim();
        if (validateIdCard18(card)) {
            return true;
        }
        if (validateIdCard15(card)) {
            return true;
        }
        String[] cardval = validateIdCard10(card);
        if (cardval != null) {
            if (cardval[2].equals("true")) {
                return true;
            }
        }
        return false;
    }

    /**
     * 验证18位身份编码是否合法
     *
     * @param idCard 身份编码
     * @return 是否合法
     */
    public static boolean validateIdCard18(String idCard) {
        boolean bTrue = false;
        if (idCard.length() == CHINA_ID_MAX_LENGTH) {
            // 前17位
            String code17 = idCard.substring(0, 17);
            // 第18位
            String code18 = idCard.substring(17, CHINA_ID_MAX_LENGTH);
            if (isNum(code17)) {
                char[] cArr = code17.toCharArray();
                int[] iCard = converCharToInt(cArr);
                int iSum17 = getPowerSum(iCard);
                // 获取校验位
                String val = getCheckCode18(iSum17);
                if (val.length() > 0) {
                    if (val.equalsIgnoreCase(code18)) {
                        bTrue = true;
                    }
                }
            }
        }
        return bTrue;
    }

    /**
     * 验证15位身份编码是否合法
     *
     * @param idCard 身份编码
     * @return 是否合法
     */
    public static boolean validateIdCard15(String idCard) {
        if (idCard.length() != CHINA_ID_MIN_LENGTH) {
            return false;
        }
        if (isNum(idCard)) {
            String proCode = idCard.substring(0, 2);
            if (cityCodes.get(proCode) == null) {
                return false;
            }
            String birthCode = idCard.substring(6, 12);
            Date birthDate = null;
            try {
                birthDate = new SimpleDateFormat("yy").parse(birthCode.substring(0, 2));
            } catch (ParseException e) {
                e.printStackTrace();
            }
            Calendar cal = Calendar.getInstance();
            if (birthDate != null) {
                cal.setTime(birthDate);
            }
            if (!valiDate(cal.get(Calendar.YEAR), Integer.valueOf(birthCode.substring(2, 4)),
                    Integer.valueOf(birthCode.substring(4, 6)))) {
                return false;
            }
        } else {
            return false;
        }
        return true;
    }

    /**
     * 验证10位身份编码是否合法
     *
     * @param idCard 身份编码
     * @return 身份证信息数组 <p> [0] - 台湾、澳门、香港 [1] - 性别(男M,女F,未知N) [2] - 是否合法(合法true,不合法false)
     * 若不是身份证件号码则返回null </p>
     */
    public static String[] validateIdCard10(String idCard) {
        String[] info = new String[3];
        String card = idCard.replaceAll("[\\(|\\)]", "");
        if (card.length() != 8 && card.length() != 9 && idCard.length() != 10) {
            return null;
        }
        if (idCard.matches("^[a-zA-Z][0-9]{9}$")) { // 台湾
            info[0] = "台湾";
            System.out.println("11111");
            String char2 = idCard.substring(1, 2);
            if (char2.equals("1")) {
                info[1] = "M";
                System.out.println("MMMMMMM");
            } else if (char2.equals("2")) {
                info[1] = "F";
                System.out.println("FFFFFFF");
            } else {
                info[1] = "N";
                info[2] = "false";
                System.out.println("NNNN");
                return info;
            }
            info[2] = validateTWCard(idCard) ? "true" : "false";
        } else if (idCard.matches("^[1|5|7][0-9]{6}\\(?[0-9A-Z]\\)?$")) { // 澳门
            info[0] = "澳门";
            info[1] = "N";
            // TODO
        } else if (idCard.matches("^[A-Z]{1,2}[0-9]{6}\\(?[0-9A]\\)?$")) { // 香港
            info[0] = "香港";
            info[1] = "N";
            info[2] = validateHKCard(idCard) ? "true" : "false";
        } else {
            return null;
        }
        return info;
    }

    /**
     * 验证台湾身份证号码
     *
     * @param idCard 身份证号码
     * @return 验证码是否符合
     */
    public static boolean validateTWCard(String idCard) {
        String start = idCard.substring(0, 1);
        String mid = idCard.substring(1, 9);
        String end = idCard.substring(9, 10);
        Integer iStart = twFirstCode.get(start);
        Integer sum = iStart / 10 + (iStart % 10) * 9;
        char[] chars = mid.toCharArray();
        Integer iflag = 8;
        for (char c : chars) {
            sum = sum + Integer.valueOf(c + "") * iflag;
            iflag--;
        }
        return (sum % 10 == 0 ? 0 : (10 - sum % 10)) == Integer.valueOf(end);
    }

    /**
     * 验证香港身份证号码(存在Bug，部份特殊身份证无法检查) <p> 身份证前2位为英文字符，如果只出现一个英文字符则表示第一位是空格，对应数字58 前2位英文字符A-Z分别对应数字10-35
     * 最后一位校验码为0-9的数字加上字符"A"，"A"代表10 </p> <p> 将身份证号码全部转换为数字，分别对应乘9-1相加的总和，整除11则证件号码有效 </p>
     *
     * @param idCard 身份证号码
     * @return 验证码是否符合
     */
    public static boolean validateHKCard(String idCard) {
        String card = idCard.replaceAll("[\\(|\\)]", "");
        Integer sum;
        if (card.length() == 9) {
            sum = ((int) card.substring(0, 1).toUpperCase().toCharArray()[0] - 55) * 9
                    + ((int) card.substring(1, 2).toUpperCase().toCharArray()[0] - 55) * 8;
            card = card.substring(1, 9);
        } else {
            sum = 522 + ((int) card.substring(0, 1).toUpperCase().toCharArray()[0] - 55) * 8;
        }
        String mid = card.substring(1, 7);
        String end = card.substring(7, 8);
        char[] chars = mid.toCharArray();
        Integer iflag = 7;
        for (char c : chars) {
            sum = sum + Integer.valueOf(c + "") * iflag;
            iflag--;
        }
        if (end.toUpperCase().equals("A")) {
            sum = sum + 10;
        } else {
            sum = sum + Integer.valueOf(end);
        }
        return (sum % 11 == 0);
    }

    /**
     * 将字符数组转换成数字数组
     *
     * @param ca 字符数组
     * @return 数字数组
     */
    public static int[] converCharToInt(char[] ca) {
        int len = ca.length;
        int[] iArr = new int[len];
        try {
            for (int i = 0; i < len; i++) {
                iArr[i] = Integer.parseInt(String.valueOf(ca[i]));
            }
        } catch (NumberFormatException e) {
            e.printStackTrace();
        }
        return iArr;
    }

    /**
     * 将身份证的每位和对应位的加权因子相乘之后，再得到和值
     *
     * @param iArr 身份证的数值表示
     * @return 身份证编码。
     */
    public static int getPowerSum(int[] iArr) {
        int iSum = 0;
        if (power.length == iArr.length) {
            for (int i = 0; i < iArr.length; i++) {
                for (int j = 0; j < power.length; j++) {
                    if (i == j) {
                        iSum = iSum + iArr[i] * power[j];
                    }
                }
            }
        }
        return iSum;
    }

    /**
     * 将power和值与11取模获得余数进行校验码判断
     *
     * @param iSum pow 余数比较
     * @return 校验位
     */
    public static String getCheckCode18(int iSum) {
        String sCode = "";
        switch (iSum % 11) {
            case 10:
                sCode = "2";
                break;
            case 9:
                sCode = "3";
                break;
            case 8:
                sCode = "4";
                break;
            case 7:
                sCode = "5";
                break;
            case 6:
                sCode = "6";
                break;
            case 5:
                sCode = "7";
                break;
            case 4:
                sCode = "8";
                break;
            case 3:
                sCode = "9";
                break;
            case 2:
                sCode = "x";
                break;
            case 1:
                sCode = "0";
                break;
            case 0:
                sCode = "1";
                break;
        }
        return sCode;
    }

    /**
     * 根据身份编号获取年龄
     *
     * @param idCard 身份编号
     * @return 年龄
     */
    public static int getAgeByIdCard(String idCard) {
        int iAge;
        if (idCard.length() == CHINA_ID_MIN_LENGTH) {
            idCard = conver15CardTo18(idCard);
        }
        String year = idCard.substring(6, 10);
        Calendar cal = Calendar.getInstance();
        int iCurrYear = cal.get(Calendar.YEAR);
        iAge = iCurrYear - Integer.valueOf(year);
        return iAge;
    }

    /**
     * 根据身份编号获取生日
     *
     * @param idCard 身份编号
     * @return 生日(yyyyMMdd)
     */
    public static String getBirthByIdCard(String idCard) {
        Integer len = idCard.length();
        if (len < CHINA_ID_MIN_LENGTH) {
            return null;
        } else if (len == CHINA_ID_MIN_LENGTH) {
            idCard = conver15CardTo18(idCard);
        }
        return idCard.substring(6, 14);
    }

    /**
     * 根据身份编号获取生日年
     *
     * @param idCard 身份编号
     * @return 生日(yyyy)
     */
    public static Short getYearByIdCard(String idCard) {
        Integer len = idCard.length();
        if (len < CHINA_ID_MIN_LENGTH) {
            return null;
        } else if (len == CHINA_ID_MIN_LENGTH) {
            idCard = conver15CardTo18(idCard);
        }
        return Short.valueOf(idCard.substring(6, 10));
    }

    /**
     * 根据身份编号获取生日月
     *
     * @param idCard 身份编号
     * @return 生日(MM)
     */
    public static Short getMonthByIdCard(String idCard) {
        Integer len = idCard.length();
        if (len < CHINA_ID_MIN_LENGTH) {
            return null;
        } else if (len == CHINA_ID_MIN_LENGTH) {
            idCard = conver15CardTo18(idCard);
        }
        return Short.valueOf(idCard.substring(10, 12));
    }

    /**
     * 根据身份编号获取生日天
     *
     * @param idCard 身份编号
     * @return 生日(dd)
     */
    public static Short getDateByIdCard(String idCard) {
        Integer len = idCard.length();
        if (len < CHINA_ID_MIN_LENGTH) {
            return null;
        } else if (len == CHINA_ID_MIN_LENGTH) {
            idCard = conver15CardTo18(idCard);
        }
        return Short.valueOf(idCard.substring(12, 14));
    }

    /**
     * 根据身份编号获取性别
     *
     * @param idCard 身份编号
     * @return 性别(M男, F女, N未知)
     */
    public static String getGenderByIdCard(String idCard) {
        String sGender = "N";
        if (idCard.length() == CHINA_ID_MIN_LENGTH) {
            idCard = conver15CardTo18(idCard);
        }
        String sCardNum = idCard.substring(16, 17);
        if (Integer.parseInt(sCardNum) % 2 != 0) {
            sGender = "M";
        } else {
            sGender = "F";
        }
        return sGender;
    }

    /**
     * 根据身份编号获取户籍省份
     *
     * @param idCard 身份编码
     * @return 省级编码。
     */
    public static String getProvinceByIdCard(String idCard) {
        int len = idCard.length();
        String sProvince;
        String sProvinNum = "";
        if (len == CHINA_ID_MIN_LENGTH || len == CHINA_ID_MAX_LENGTH) {
            sProvinNum = idCard.substring(0, 2);
        }
        sProvince = cityCodes.get(sProvinNum);
        return sProvince;
    }

    /**
     * 数字验证
     *
     * @param val 字符串数字
     * @return 提取的数字。
     */
    private static boolean isNum(String val) {
        return !(val == null || "".equals(val)) && val.matches("^[0-9]*$");
    }

    /**
     * 验证小于当前日期 是否有效
     *
     * @param iYear  待验证日期(年)
     * @param iMonth 待验证日期(月 1-12)
     * @param iDate  待验证日期(日)
     * @return 是否有效
     */
    private static boolean valiDate(int iYear, int iMonth, int iDate) {
        Calendar cal = Calendar.getInstance();
        int year = cal.get(Calendar.YEAR);
        int datePerMonth;
        if (iYear < MIN || iYear >= year) {
            return false;
        }
        if (iMonth < 1 || iMonth > 12) {
            return false;
        }
        switch (iMonth) {
            case 4:
            case 6:
            case 9:
            case 11:
                datePerMonth = 30;
                break;
            case 2:
                boolean dm = ((iYear % 4 == 0 && iYear % 100 != 0) || (iYear % 400 == 0))
                        && (iYear > MIN && iYear < year);
                datePerMonth = dm ? 29 : 28;
                break;
            default:
                datePerMonth = 31;
        }
        return (iDate >= 1) && (iDate <= datePerMonth);
    }
}


/*
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
 *
 * Copyright (c) 2013-2014 sagyf Yang. The Four Group.
 */

package goja.mvc.render.ftl.kit;

import goja.mvc.render.ftl.layout.BlockDirective;
import goja.mvc.render.ftl.layout.ExtendsDirective;
import goja.mvc.render.ftl.layout.OverrideDirective;
import goja.mvc.render.ftl.layout.SuperDirective;

import freemarker.core.Environment;
import freemarker.template.Configuration;
import freemarker.template.TemplateException;
import freemarker.template.TemplateModelException;

import org.apache.commons.lang3.StringUtils;

import java.util.Map;

/**
 *
 Freemarker 的工具类.

 *
 * @author poplar.yfyang
 * @version 1.0 2012-04-20 上午7:30
 * @since JDK 1.5
 */
public class DirectiveKit {

    /**
     * override的标记
     */
    public static final String BLOCK                 = "__ftl_override__";
    public static final String OVERRIDE_CURRENT_NODE = "__ftl_override_current_node";

    /**
     * 返回override的标记与对应的名称信息
     *
     * @param name 名称
     * @return 标记名称信息
     */
    public static String getOverrideVariableName(String name) {
        return BLOCK + name;
    }

    public static void exposeIflytekMacros(Configuration conf) {
        conf.setSharedVariable(BlockDirective.DIRECTIVE_NAME, new BlockDirective());
        conf.setSharedVariable(ExtendsDirective.DIRECTIVE_NAME, new ExtendsDirective());
        conf.setSharedVariable(OverrideDirective.DIRECTIVE_NAME, new OverrideDirective());
        conf.setSharedVariable(SuperDirective.DIRECTIVE_NAME, new SuperDirective());
    }

    /**
     * 取得参数params中必须存在的指定Key的值，如果不存在，抛出异常
     *
     * @param params 参数Map
     * @param key    必须存在的Key
     * @return 对应key的值
     * @throws freemarker.template.TemplateException 异常，如果key不存在params中则会出现异常
     */
    @SuppressWarnings("rawtypes")
    public static String getRequiredParam(Map params, String key) throws TemplateException {
        Object value = params.get(key);
        if (value == null || StringUtils.isEmpty(value.toString())) {
            throw new TemplateModelException("not found required parameter:" + key + " for directive");
        }
        return value.toString();
    }

    /**
     * 取得params中指定Key的值，如果不存在返回默认信息
     *
     * @param params       参数Map
     * @param key          key信息
     * @param defaultValue 默认值
     * @return 对应Key的值，不存在返回默认
     */
    @SuppressWarnings("rawtypes")
    public static String getParam(Map params, String key, String defaultValue) {
        Object value = params.get(key);
        return value == null ? defaultValue : value.toString();
    }

    /**
     * 从Freemarker的运行环境中，取得指定名称的覆盖内容渲染器
     *
     * @param env  运行环境
     * @param name 指定名称
     * @return 覆盖内容渲染器
     * @throws freemarker.template.TemplateModelException 模板模型异常
     */
    public static TemplateDirectiveBodyOverrideWraper getOverrideBody(Environment env, String name)
            throws TemplateModelException {
        return (TemplateDirectiveBodyOverrideWraper)
                env.getVariable(DirectiveKit.getOverrideVariableName(name));
    }

    /**
     * 对指定的Body的渲染器，指定其父亲的内容区域
     *
     * @param topBody      内容渲染器
     * @param overrideBody 需要覆盖的内容渲染器
     */
    public static void setTopBodyForParentBody(TemplateDirectiveBodyOverrideWraper topBody,
                                               TemplateDirectiveBodyOverrideWraper overrideBody) {
        TemplateDirectiveBodyOverrideWraper parent = overrideBody;
        while (parent.parentBody != null) {
            parent = parent.parentBody;
        }
        parent.parentBody = topBody;
    }
}


/*
 * Copyright (C) 2011-2013 Eiichiro Uchiumi. All Rights Reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.eiichiro.bootleg;

import java.io.IOException;

import javax.servlet.http.HttpServletResponse;

import org.eiichiro.reverb.lang.UncheckedException;
import org.eiichiro.reverb.reflection.MethodInvocation;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.google.common.base.Predicate;

/**
 * HTTP request processing pipeline stage to invoke Web endpoint method.
 *
 * @author Eiichiro Uchiumi
 */
public class Invoke implements Predicate<WebContext> {

	private Logger logger = LoggerFactory.getLogger(getClass());

	/**
	 * Invokes Web endpoint method.
	 * This method processes the request as the following steps:
	 *

	 *
Gets Web endpoint class by invoking {@code Method#getDeclaringClass()}
	 * of the Web endpoint method set on the current HTTP request processing
	 * context and instantiates it with the {@link Instantiator} that the
	 * {@link Configuration#instantiator()} returns.

	 *
Invokes Web endpoint method on the constructed instance with the
	 * parameters set on the current HTTP request processing context and sets
	 * the invocation result to the current HTTP request processing context.

	 *
If the invocation is failed with {@code WebException}, this method
	 * sends HTTP response with the status code the exception has.

	 *
If the invocation is failed for any reasons, this method sends HTTP
	 * response with the status code 500 (INTERNAL_SERVER_ERROR).

	 *

	 *
	 * @param context HTTP request processing context.
	 */
	public boolean apply(WebContext context) {
		WebException e = null;

		try {
			context.result(new MethodInvocation<Object>(
					context.method(),
					instantiate(context.method().getDeclaringClass()),
					context.parameters().toArray()).proceed());
			return true;
		} catch (WebException exception) {
			e = exception;
		} catch (Throwable throwable) {
			e = new EndpointInvocationFailedException(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, throwable);
		}

		logger.warn("Failed to invoke Web endpoint", e);

		try {
			context.response().sendError(e.status());
			return false;
		} catch (IOException exception) {
			throw new UncheckedException(exception);
		}
	}

	protected <T> T instantiate(Class<T> endpoint) throws Exception {
		return endpoint.newInstance();
	}

}


/*
    Copyright (C) 2016 Omkar Todkar

    Licensed under the Apache License, Version 2.0 (the "License");
    you may not use this file except in compliance with the License.
    You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

    Unless required by applicable law or agreed to in writing, software
    distributed under the License is distributed on an "AS IS" BASIS,
    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    See the License for the specific language governing permissions and
    limitations under the License.
*/

package co.omkar.utility.opermission;

import android.content.Intent;
import android.content.pm.PackageManager;
import android.os.Build;
import android.support.annotation.NonNull;
import android.support.v4.app.FragmentActivity;
import android.support.v4.content.LocalBroadcastManager;
import android.util.Log;

import java.lang.annotation.Annotation;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import co.omkar.utility.opermission.annotation.DeniedPermission;
import co.omkar.utility.opermission.annotation.GrantedPermission;
import co.omkar.utility.opermission.annotation.OPermission;
import co.omkar.utility.opermission.bean.PermBean;
import co.omkar.utility.opermission.bean.Permission;
import co.omkar.utility.opermission.bean.Result;
import co.omkar.utility.opermission.dialog.PermissionDialogFragment;

/**
 *
Request permission is builder class to
 * prepare a permission request.

 * Created on 12/08/16.
 *
 * @author Omkar Todkar
 */
public final class RequestPermission {
    private static final String TAG = "oPermission";

    /**
     * Local broadcast on permission result.
     */
    public static final String PERMISSION_RESULT_BROADCAST = "co.omkar.utility.opermission.result";

    /**
     * Intent bundle key to get array of granted permissions
     * from broadcast intent.
     */
    public static final String GRANTED = "KeyGranted";

    /**
     * Intent bundle key to get array of denied permissions
     * from broadcast intent.
     */
    public static final String DENIED = "KeyDenied";

    /**
     * An activity instance.
     */
    private static FragmentActivity mActivity;

    /**
     * An Object where annotation classes are declared.
     */
    private static Object mBase;

    /**
     * A wrapper class with bundle of permissions
     * and respective messages.
     */
    private static PermBean permBean;

    /**
     * A pair of permissions and its results to invoke respective methods.
     */
    private static HashMap<Permission, Result> resultMap;

    /**
     * A permission request code.
     */
    private static int requestCode = 16989;

    /**
     * Private constructor of Request Permission.
     *
     * @param mActivity instance of activity in where
     *                  permissions are asked.
     */
    private RequestPermission(FragmentActivity mActivity) {
        RequestPermission.mActivity = mActivity;
    }

    /**
     * Initializing method.
     *
     * @param activity instance of activity where permissions
     *                 are asked.
     * @return prepared object of RequestPermission.
     */
    public static RequestPermission on(FragmentActivity activity) {
        return new RequestPermission(activity);
    }

    /**
     * Set preferred permission request code.
     *
     * @param code Any positive 16 bit {@link Integer} value.
     * @return prepared object of RequestPermission.
     */
    public RequestPermission code(int code) {
        if (code < 1)
            throw new NumberFormatException("Request code must be non-zero positive integer");
        else
            RequestPermission.requestCode = code;
        return this;
    }

    /**
     * Set a permission and message wrapper class {@link PermBean}
     * with desired permission/s and respective messages.
     *
     * @param permBean A wrapper class containing
     *                 permissions to be asked and
     *                 respective message to be shown.
     * @return modified object of RequestPermission containing
     * permissions to be asked and respective messages to be shown.
     */
    public RequestPermission with(PermBean permBean) {
        if (permBean.getPermissions().isEmpty()) {
            throw new NullPointerException("Permission and Message collection cannot be null !");
        }
        RequestPermission.permBean = permBean;
        return this;
    }

    /**
     * Turn off oPermission logs while requesting permissions.
     *
     * @return modified instance of RequestPermission with debug ON/OFF.
     */
    public RequestPermission debug() {
        mLog.setDebugMode(true);
        return this;
    }

    /**
     * Set a initialized class where you declared result annotated methods.
     * Example - instance of Presenter, ViewModel, etc.
     *
     * @param obj instance of Class containing {@link GrantedPermission}
     *            / {@link DeniedPermission} methods.
     * @return Modified instance of RequestPermission.
     */
    public RequestPermission setResultTarget(Object obj) {
        RequestPermission.mBase = obj;
        return this;
    }

    /**
     *
Execute request for all provided permissions. It checks if permissions
     * are already granted or required to grant.

     *
Only if only API is greater than 23 (i.e. Marshmallows or later).

     */
    public void request() {
        mLog.i(TAG, "Requesting.........");
        resultMap = new LinkedHashMap<>(permBean.size());

        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M
                && isPermissionRequired(permBean)) {

            // if result target is not set use activity as default.
            if (RequestPermission.mBase == null) {
                RequestPermission.mBase = mActivity;
            }
            mLog.i(TAG, "On permission result " + mBase.getClass().getSimpleName()
                    + " class methods will be executed.");

            PermBean bean = new PermBean();
            Map<Permission, String> map = permBean.getPermissions();
            for (Map.Entry<Permission, String> m : map.entrySet()) {
                if (mActivity.checkSelfPermission(m.getKey().toString()) != PackageManager.PERMISSION_GRANTED) {
                    bean.put(m.getKey(), m.getValue());
                    mLog.i(TAG, m.getKey().name() + " requires permission");
                } else {
                    resultMap.put(m.getKey(), Result.GRANTED);
                }
            }

            // ask permissions for granted methods.
            if (bean.size() > 0) {
                showDialog(bean);
            }
        } else {
            for (Map.Entry<Permission, String> m : permBean.getPermissions().entrySet()) {
                resultMap.put(m.getKey(), Result.GRANTED);
            }
            try {
                invokeAnnotatedMethods(resultMap);
            } catch (InvocationTargetException e) {
                mLog.e(TAG, e.getMessage(), e);
            } catch (IllegalAccessException e) {
                mLog.e(TAG, e.getMessage(), e);
            }
            mLog.i(TAG, "request: Redundant");
        }
    }

    /**
     *
Check weather single or multiple permissions requires grant.

     * Use instead {@link RequestPermission#request()} directly.
     *
     * @param permBean {@link PermBean} wrapper class.
     * @return boolean value if any one of permission requires to be asked.
     */
    private static boolean isPermissionRequired(PermBean permBean) {
        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
            return false;
        }
        if (permBean.size() > 0) {
            Map<Permission, String> map = permBean.getPermissions();
            for (Permission permission : map.keySet()) {
                int status = mActivity.checkSelfPermission(permission.toString());
                if (status != PackageManager.PERMISSION_GRANTED) return true;
            }
        }
        return false;
    }

    /**
     * Show dialog fragment to show dialogs before asking permission.
     * Or you can set to show dialogs only when user denied permission earlier.
     *
     * @param permBean {@link PermBean} wrapper class
     *                 with Permission and respective messages.
     */
    private void showDialog(PermBean permBean) {
        PermissionDialogFragment fragment = PermissionDialogFragment.getInstance(permBean, requestCode);
        fragment.show(mActivity.getSupportFragmentManager(), TAG);
    }

    /**
     * Get on Request Permissions Results, invoke annotated methods and send a local broadcast of results.
     *
     * @param requestCode  Auto Generated request code.
     * @param permissions  Permissions asked.
     * @param grantResults Result of permissions asked.
     */
    public static void onResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
        if (RequestPermission.requestCode == requestCode) {
            /* Sort granted and denied permissions in array list */
            int count = permissions.length;

            List<String> granted = new ArrayList<>(count);
            List<String> denied = new ArrayList<>(count);

            for (int k = 0; k < count; k++) {
                resultMap.put(Permission.get(permissions[k]), Result.get(grantResults[k]));
                if (grantResults[k] == PackageManager.PERMISSION_GRANTED) {
                    granted.add(permissions[k]);

                } else if (grantResults[k] == PackageManager.PERMISSION_DENIED) {
                    denied.add(permissions[k]);

                }
            }

            String[] grantedArray = granted.toArray(new String[granted.size()]);
            String[] deniedArray = denied.toArray(new String[denied.size()]);

            // forward to invoke annotated methods.
            try {
                invokeAnnotatedMethods(resultMap);
            } catch (IllegalAccessException e) {
                mLog.e(TAG, e.getMessage(), e);
            } catch (InvocationTargetException e) {
                mLog.e(TAG, e.getMessage(), e);
            }

            /* Send local broadcast on permissions result. */
            Intent intent = new Intent(PERMISSION_RESULT_BROADCAST);
            intent.putExtra(GRANTED, grantedArray);
            intent.putExtra(DENIED, deniedArray);
            LocalBroadcastManager.getInstance(mActivity).sendBroadcast(intent);
        }
    }

    /**
     * Invoke annotated methods in provided class.
     *
     * @param resultMap prepared Map of Permissions and respective results.
     * @throws InvocationTargetException
     * @throws IllegalAccessException
     */
    private static void invokeAnnotatedMethods(HashMap<Permission, Result> resultMap)
            throws InvocationTargetException, IllegalAccessException {
        Method[] methods = getBase().getClass().getDeclaredMethods();

        // check all methods from provided class or by default provided activity.
        for (Method method : methods) {

            if (method != null && method.isAnnotationPresent(GrantedPermission.class)) {
                GrantedPermission granted = method.getAnnotation(GrantedPermission.class);

                if (granted != null) {

                    /* Check single value annotations */
                    if (!granted.permission().equals("") || granted.value() != Permission.NONE) {
                        for (Map.Entry<Permission, Result> permResult : resultMap.entrySet()) {

                            // invoke method if String permission is annotated.
                            if ((granted.permission().equals(permResult.getKey().toString()) ||
                                    granted.value() == permResult.getKey())
                                    && Result.GRANTED == permResult.getValue()) {
                                method.invoke(getBase());
                                mLog.i(TAG, "invoking grant method: " + method.getName());
                            }
                        }
                        continue;
                    }

                    /* Check array fields in annotations */
                    if (granted.values().length > 0) {
                        if (allValuesGranted(granted.values(), resultMap)) {
                            method.invoke(getBase());
                            mLog.i(TAG, "invoking grant method: " + method.getName());
                        }
                        continue;
                    }

                    if (granted.permissions().length > 0) {
                        if (allValuesGranted(granted.permissions(), resultMap)) {
                            method.invoke(getBase());
                            mLog.i(TAG, "invoking grant method: " + method.getName());
                        }
                    }
                }
            } else if (method != null && method.isAnnotationPresent(DeniedPermission.class)) {
                DeniedPermission denied = method.getAnnotation(DeniedPermission.class);

                if (denied != null) {

                    /* Check single value annotations */
                    if (!denied.permission().equals("") || denied.value() != Permission.NONE) {
                        for (Map.Entry<Permission, Result> permResult : resultMap.entrySet()) {

                            // invoke method if String permission is annotated.
                            if ((denied.permission().equals(permResult.getKey().toString())
                                    || denied.value() == permResult.getKey())
                                    && Result.DENIED == permResult.getValue()) {
                                method.invoke(getBase());
                                mLog.i(TAG, "invoking denied method: " + method.getName());
                            }
                        }
                        continue;
                    }

                    /* Check array fields in annotations */
                    if (denied.values().length > 0) {
                        if (anyValueDenied(denied.values(), resultMap)) {
                            method.invoke(getBase());
                            mLog.i(TAG, "invoking denied method: " + method.getName());
                        }
                        continue;
                    }

                    if (denied.permissions().length > 0) {
                        if (anyValueDenied(denied.permissions(), resultMap)) {
                            mLog.i(TAG, "invoking denied method: " + method.getName());
                            method.invoke(getBase());
                        }
                    }
                }
            } else if (method != null && method.isAnnotationPresent(OPermission.class)) {
                OPermission oPermission = method.getAnnotation(OPermission.class);
                final Annotation[][] paramAnnotations = method.getParameterAnnotations();
                final Class[] paramTypes = method.getParameterTypes();

                if (oPermission != null
                        && paramAnnotations[0][0] instanceof co.omkar.utility.opermission.annotation.Result
                        && (paramTypes[0] == boolean.class
                        || paramTypes[0] == Boolean.class)) {

                    /* Check single value annotations */
                    for (Map.Entry<Permission, Result> permResult : resultMap.entrySet()) {

                        // invoke method if String permission is annotated.
                        if (oPermission.permission().equals(permResult.getKey().toString())
                                || oPermission.value().equals(permResult.getKey())) {

                            switch (permResult.getValue()) {

                                // Permission is granted.
                                case GRANTED:
                                    method.invoke(getBase(), true);
                                    mLog.i(TAG, "invoking method: " + method.getName());
                                    break;

                                // Permission is denied.
                                case DENIED:
                                    method.invoke(getBase(), false);
                                    mLog.i(TAG, "invoking method: " + method.getName());
                                    break;
                            }
                        }
                    }

                        /* Check array fields in annotations */
                    if (oPermission.values().length > 0) {
                        if (allValuesGranted(oPermission.values(), resultMap)) {
                            method.invoke(getBase(), true);
                            mLog.i(TAG, "invoking as granted method: " + method.getName());
                        } else {
                            method.invoke(getBase(), false);
                            mLog.i(TAG, "invoking as denied method: " + method.getName());
                        }
                        continue;
                    }

                    if (oPermission.permissions().length > 0) {
                        if (allValuesGranted(oPermission.permissions(), resultMap)) {
                            method.invoke(getBase(), true);
                            mLog.i(TAG, "invoking as granted method: " + method.getName());
                        } else {
                            method.invoke(getBase(), false);
                            mLog.i(TAG, "invoking as denied method: " + method.getName());
                        }
                    }
                }
            }

        }
    }

    /**
     * Check if annotated result permissions contains all values from annotated array.
     * Then it also check if all permissions in annotated method value array are granted.
     *
     * @param values    Annotated value ({@link Permission}) array.
     * @param resultMap Permission and respective result map.
     * @return false if any of above condition fails.
     */
    private static boolean allValuesGranted(Object[] values, HashMap<Permission, Result> resultMap) {
        if (values instanceof Permission[]) {
            Set<Permission> valueSet = new HashSet<>(Arrays.asList((Permission[]) values));
            if (resultMap.keySet().containsAll(valueSet)) {
                for (Object value : values) {
                    if (Result.GRANTED != resultMap.get((Permission) value)) {
                        mLog.i(TAG, "denied - " + value.toString());
                        return false;
                    }
                }
                return true;
            }
        } else if (values instanceof String[]) {
            Set<String> valueSet = new HashSet<>(Arrays.asList((String[]) values));
            Set<String> permission = new HashSet<>();
            for (Permission perm : resultMap.keySet()) {
                permission.add(perm.toString());
            }
            if (permission.containsAll(valueSet)) {
                for (Object value : values) {
                    if (Result.GRANTED != resultMap.get(Permission.get(String.valueOf(value)))) {
                        mLog.i(TAG, "denied - " + value);
                        return false;
                    }
                }
                return true;
            }
        }
        return false;
    }

    /**
     * Check if annotated result permissions contains all values from annotated array.
     * Then it also check if any permission in annotated method value array is denied.
     *
     * @param values    ({@link Permission} or {@link String}) array.
     * @param resultMap Permission and respective result map.
     * @return false if first statement is not satisfied else true if both are satisfied.
     */
    private static boolean anyValueDenied(Object[] values, HashMap<Permission, Result> resultMap) {
        if (values instanceof Permission[]) {
            Set<Permission> valueSet = new LinkedHashSet<>(Arrays.asList((Permission[]) values));
            if (resultMap.keySet().containsAll(valueSet)) {
                for (Object value : values) {
                    if (Result.DENIED == resultMap.get((Permission) value)) {
                        mLog.i(TAG, "denied - " + value.toString());
                        return true;
                    }
                }
            }
        } else if (values instanceof String[]) {
            Set<String> valueSet = new HashSet<>(Arrays.asList((String[]) values));
            Set<String> permissionSet = new HashSet<>();
            for (Permission perm : resultMap.keySet()) {
                permissionSet.add(perm.toString());
            }
            if (permissionSet.containsAll(valueSet)) {
                for (Object value : values) {
                    if (Result.DENIED == resultMap.get(Permission.get((String) value))) {
                        mLog.i(TAG, "denied - " + value);
                        return true;
                    }
                }
            }
        }
        return false;
    }

    private static Object getBase() {
        return RequestPermission.mBase;
    }

    /**
     *
Controller logger.

     * Created on 19/08/16.
     *
     * @author Omkar Todkar.
     */
    private static final class mLog {

        private static boolean mode;

        private static boolean isMode() {
            return mode;
        }

        public static void setDebugMode(boolean mode) {
            mLog.mode = mode;
        }

        public static int v(String tag, String msg) {
            return isMode() ? Log.v(tag, msg) : -1;
        }

        public static int v(String tag, String msg, Throwable tr) {
            return isMode() ? Log.v(tag, msg, tr) : -1;
        }

        public static int d(String tag, String msg) {
            return isMode() ? Log.d(tag, msg) : -1;
        }

        public static int d(String tag, String msg, Throwable tr) {
            return isMode() ? Log.d(tag, msg, tr) : -1;
        }

        public static int i(String tag, String msg) {
            return isMode() ? Log.i(tag, msg) : -1;
        }

        public static int i(String tag, String msg, Throwable tr) {
            return isMode() ? Log.i(tag, msg, tr) : -1;
        }

        public static int w(String tag, String msg) {
            return isMode() ? Log.w(tag, msg) : -1;
        }

        public static int w(String tag, String msg, Throwable tr) {
            return isMode() ? Log.w(tag, msg, tr) : -1;
        }

        public static int w(String tag, Throwable tr) {
            return isMode() ? Log.w(tag, tr) : -1;
        }

        public static int e(String tag, String msg) {
            return isMode() ? Log.e(tag, msg) : -1;
        }

        public static int e(String tag, String msg, Throwable tr) {
            return isMode() ? Log.e(tag, msg, tr) : -1;
        }

        public static int wtf(String tag, String msg) {
            return isMode() ? Log.e(tag, msg) : -1;
        }

        public static int wtf(String tag, Throwable tr) {
            return isMode() ? Log.wtf(tag, tr) : -1;
        }

        public static int wtf(String tag, String msg, Throwable tr) {
            return isMode() ? Log.wtf(tag, msg, tr) : -1;
        }

        public static String getStackTraceString(Throwable tr) {
            return isMode() ? Log.getStackTraceString(tr) : null;
        }

        public static int println(int priority, String tag, String msg) {
            return isMode() ? Log.println(priority, tag, msg) : -1;
        }
    }
}


package com.truward.brikar.client.rest.support;

import com.truward.brikar.client.rest.RestServiceBinder;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpMethod;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.client.RestOperations;
import org.springframework.web.util.UriTemplate;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.lang.annotation.Annotation;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.*;

/**
 * @author Alexander Shabanov
 */
public class StandardRestServiceBinder implements RestServiceBinder {
  private final RestOperations restOperations;

  public StandardRestServiceBinder(@Nonnull RestOperations restOperations) {
    this.restOperations = Objects.requireNonNull(restOperations, "restOperations");
  }

  @Override
  @Nonnull
  public <T> T createClient(@Nonnull String serviceBaseUrl,
                            @Nonnull Class<T> restServiceClass,
                            @Nonnull Class<?> ... extraClasses) {
    final Map<Method, MethodInvocationHandler> handlerMap = new HashMap<>();
    for (final Method method : restServiceClass.getMethods()) {
      handlerMap.put(method, getMethodInvocationHandler(method, restServiceClass, serviceBaseUrl));
    }

    final Class[] classes = Arrays.copyOf(extraClasses, extraClasses.length + 1);
    classes[extraClasses.length] = restServiceClass;

    final Object o = Proxy.newProxyInstance(Thread.currentThread().getContextClassLoader(),
        classes, new MethodMapBasedInvocationHandler(handlerMap));
    return restServiceClass.cast(o);
  }

  @Override
  @Nonnull
  public <T> T createClient(@Nonnull URI serviceBaseUrl,
                     @Nonnull Class<T> restServiceClass,
                     @Nonnull Class<?> ... extraClasses) {
    return createClient(serviceBaseUrl.toString(), restServiceClass, extraClasses);
  }

  //
  // Private
  //

  /*
   * Creates an instance of an interface which is supposed to handle method calls in the proxy.
   */
  @Nonnull
  private MethodInvocationHandler getMethodInvocationHandler(@Nonnull Method method,
                                                             @Nonnull final Class<?> clazz,
                                                             @Nonnull String serviceBaseUrl) {
    // toString+hashCode+equals
    if (method.getName().equals("toString") && method.getParameterTypes().length == 0) {
      return (proxy, method1, args) -> clazz.getSimpleName();
    } else if (method.getName().equals("hashCode") && method.getParameterTypes().length == 0) {
      return new MethodInvocationHandler() {
        @Override
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
          return this.hashCode();
        }
      };
    } else if (method.getName().equals("equals") && method.getParameterTypes().length == 1 &&
        method.getParameterTypes()[0] == Object.class) {
      return (proxy, method12, args) -> proxy == args[0];
    }

    return createRequestMappingHandler(method, serviceBaseUrl);
  }

  private interface MethodInvocationHandler {
    Object invoke(Object proxy, Method method, Object[] args) throws Throwable;
  }

  private static class MethodMapBasedInvocationHandler implements InvocationHandler {
    final Map<Method, MethodInvocationHandler> handlers;

    public MethodMapBasedInvocationHandler(@Nonnull Map<Method, MethodInvocationHandler> handlers) {
      this.handlers = handlers;
    }

    @Override
    public Object invoke(Object proxy,
                         @SuppressWarnings("NullableProblems") Method method,
                         Object[] args) throws Throwable {
      final MethodInvocationHandler handler = handlers.get(method);
      if (handler == null) {
        throw new UnsupportedOperationException("Unsupported method=" + method);
      }
      return handler.invoke(proxy, method, args);
    }
  }

  @Nonnull
  private MethodInvocationHandler createRequestMappingHandler(@Nonnull Method method, @Nonnull String serviceBaseUrl) {

    final RequestMapping requestMapping = method.getAnnotation(RequestMapping.class);
    if (requestMapping == null) {
      // Request mapping is missing - just POST to the given URL with method as a parameter
      return new RestMethodInvocationHandler(HttpMethod.POST,
          new PostRequestUriExtractor(serviceBaseUrl, method.getName()),
          PostRequestBodyExtractor.INSTANCE,
          restOperations);
    }

    checkUnsupportedProperties(requestMapping);
    final RequestMethod requestMethod = getRequestMethod(requestMapping);
    final MethodParseResult parseResult = parseMethod(method, serviceBaseUrl, requestMapping);

    return new RestMethodInvocationHandler(HttpMethod.valueOf(requestMethod.name()), parseResult.uriExtractor,
        parseResult.bodyExtractor, restOperations);
  }

  @Nonnull
  private MethodParseResult parseMethod(@Nonnull Method method,
                                        @Nonnull String serviceBaseUrl,
                                        @Nonnull RequestMapping requestMapping) {
    final Annotation[][] methodParamAnnotations = method.getParameterAnnotations();

    final List<PosNamePair> posNamePairs = new ArrayList<>(10);
    final String prefixUrl = getRequestMappingPrefixUrl(requestMapping);
    final StringBuilder builder = new StringBuilder(serviceBaseUrl.length() + prefixUrl.length() + 50);
    RequestBodyExtractor bodyExtractor = EmptyRequestBodyExtractor.INSTANCE;

    builder.append(serviceBaseUrl).append(prefixUrl);

    // iterate over each method parameter's annotation
    for (int i = 0, rp = 0; i < methodParamAnnotations.length; ++i) {
      final Annotation[] annotations = methodParamAnnotations[i];
      for (final Annotation a : annotations) {
        if (a.annotationType().equals(PathVariable.class)) {
          posNamePairs.add(new PosNamePair(i, ((PathVariable) a).value()));
          continue;
        } else if (a.annotationType().equals(RequestParam.class)) {
          final String name = ((RequestParam) a).value();
          posNamePairs.add(new PosNamePair(i, name));

          // since this is a request param we need to modify URL template as well
          // this is needed to make UriTemplate handle query parameters in addition to path parameters
          builder.append(rp > 0 ? '&' : '?').append(name).append('=').append(getParamMask(builder.toString(), name));

          ++rp;
          continue;
        } else if (a.annotationType().equals(RequestBody.class)) {
          if (bodyExtractor != EmptyRequestBodyExtractor.INSTANCE) {
            // more than one request body? - possible wrong mapping
            throw new UnsupportedOperationException("Can not have more than one RequestBody, offending " +
                "parameter number: " + (i + 1));
          }

          bodyExtractor = new PosArgBodyExtractor(i);
          continue;
        }

        throw new UnsupportedOperationException("Unable to create method handler because of parameter number "  +
            (i + 1) + ": PathVariable, RequestParam or RequestBody annotation is expected");
      }
    }

    final UriExtractor uriExtractor;
    if (posNamePairs.isEmpty()) {
      // optimized version
      uriExtractor = new EmptyParamsUriExtractor(builder.toString());
    } else {
      uriExtractor = new StandardUriExtractor(builder.toString(), posNamePairs.toArray(new PosNamePair[posNamePairs.size()]));
    }

    return new MethodParseResult(uriExtractor, bodyExtractor);
  }

  private static String getParamMask(@Nonnull String partialUrl, @Nonnull String argName) {
    final StringBuilder mask = new StringBuilder(argName.length() + 3);
    for (int i = 0;; ++i) {
      mask.setLength(0);
      mask.append('{').append(argName);
      if (i > 0) {
        mask.append(i);
      }
      mask.append('}');

      if (!partialUrl.contains(mask)) {
        return mask.toString();
      }
    }
  }

  private static final class MethodParseResult {
    final UriExtractor uriExtractor;
    final RequestBodyExtractor bodyExtractor;

    public MethodParseResult(@Nonnull UriExtractor uriExtractor, @Nonnull RequestBodyExtractor bodyExtractor) {
      this.uriExtractor = uriExtractor;
      this.bodyExtractor = bodyExtractor;
    }
  }

  @Nonnull
  private String getRequestMappingPrefixUrl(@Nonnull RequestMapping requestMapping) {
    final String[] mappings = requestMapping.value();
    if (mappings.length == 1) {
      return mappings[0];
    } else if (mappings.length == 0) {
      return "";
    }

    throw new UnsupportedOperationException("RequestMapping.value is not expected to have more than one value");
  }

  private void checkUnsupportedProperties(@Nonnull RequestMapping requestMapping) {
    if (requestMapping.consumes().length > 0) {
      throw new UnsupportedOperationException("RequestMapping.consumes is not supported");
    }

    if (requestMapping.headers().length > 0) {
      throw new UnsupportedOperationException("RequestMapping.headers is not supported");
    }

    if (requestMapping.produces().length > 0) {
      throw new UnsupportedOperationException("RequestMapping.consumes is not supported");
    }

    if (requestMapping.params().length > 0) {
      throw new UnsupportedOperationException("RequestMapping.params is not supported");
    }
  }

  @Nonnull
  private RequestMethod getRequestMethod(@Nonnull RequestMapping requestMapping) {
    final RequestMethod[] methods = requestMapping.method();
    if (methods.length == 1) {
      return methods[0];
    } else if (methods.length == 0) {
      return RequestMethod.GET; // default method
    }

    throw new UnsupportedOperationException("RequestMapping.methods contains more than one parameters");
  }

  private static final class RestMethodInvocationHandler implements MethodInvocationHandler {
    final HttpMethod httpMethod;
    final UriExtractor uriExtractor;
    final RequestBodyExtractor bodyExtractor;
    final RestOperations restOperations;

    public RestMethodInvocationHandler(@Nonnull HttpMethod httpMethod,
                                       @Nonnull UriExtractor uriExtractor,
                                       @Nonnull RequestBodyExtractor bodyExtractor,
                                       @Nonnull RestOperations restOperations) {
      this.httpMethod = httpMethod;
      this.uriExtractor = uriExtractor;
      this.bodyExtractor = bodyExtractor;
      this.restOperations = restOperations;
    }

    @Override
    public Object invoke(Object proxy, Method method, @Nullable Object[] args) throws Throwable {
      final URI uri = uriExtractor.extract(args);
      final Object body = bodyExtractor.extract(args);

      HttpEntity<?> entity;
      if (body == null) {
        entity = HttpEntity.EMPTY;
      } else {
        entity = new HttpEntity<>(body);
      }

      // NOTE: exchange may throw HttpServerErrorException and HttpClientErrorException
      final ResponseEntity<?> responseEntity = restOperations.exchange(uri, httpMethod, entity, method.getReturnType());
      if (responseEntity == null) {
        throw new IllegalStateException("responseEntity is null"); // should not happen
      }
      return responseEntity.getBody();
    }
  }

  //
  // RequestBodyExtractor
  //

  interface RequestBodyExtractor {
    @Nullable
    Object extract(@Nullable Object[] arguments);
  }

  private static final class PostRequestBodyExtractor implements RequestBodyExtractor {

    public static final RequestBodyExtractor INSTANCE = new PostRequestBodyExtractor();

    private PostRequestBodyExtractor() {
    }

    @Nullable
    @Override
    public Object extract(@Nullable Object[] arguments) {
      if (arguments == null || arguments.length != 1) {
        throw new IllegalStateException("Exactly one argument expected for methods that are not " +
            "annotated with RequestMapping annotation");
      }
      return arguments[0];
    }
  }

  private static final class EmptyRequestBodyExtractor implements RequestBodyExtractor {
    static final EmptyRequestBodyExtractor INSTANCE = new EmptyRequestBodyExtractor();

    @Nullable
    @Override
    public Object extract(@Nullable Object[] arguments) {
      return null;
    }
  }

  private static final class PosArgBodyExtractor implements RequestBodyExtractor {
    final int argPos;

    public PosArgBodyExtractor(int argPos) {
      this.argPos = argPos;
    }

    @Nullable
    @Override
    public Object extract(@Nullable Object[] arguments) {
      if (arguments == null || argPos >= arguments.length) {
        // should never happen as we check positional arguments before constructing this object
        throw new IllegalStateException("Unexpected arguments: too few given");
      }
      return arguments[argPos];
    }
  }

  //
  // UriExtractor
  //

  interface UriExtractor {
    @Nonnull
    URI extract(@Nullable Object[] arguments);
  }

  private static abstract class AbstractUriExtractor implements UriExtractor {
    final String baseUrl;

    public AbstractUriExtractor(@Nonnull String baseUrl) {
      this.baseUrl = baseUrl;
    }
  }

  private static final class PostRequestUriExtractor extends AbstractUriExtractor {
    final String methodName;

    public PostRequestUriExtractor(@Nonnull String baseUrl, @Nonnull String methodName) {
      super(baseUrl);
      this.methodName = methodName;
    }

    @Nonnull
    @Override
    public URI extract(@Nullable Object[] arguments) {
      // suppressed to have StringBuilder with estimated length
      //noinspection StringBufferReplaceableByString
      final StringBuilder builder = new StringBuilder(1 + baseUrl.length() + methodName.length());
      builder.append(baseUrl).append('/').append(methodName);
      return URI.create(builder.toString());
    }
  }

  private static final class EmptyParamsUriExtractor extends AbstractUriExtractor {

    public EmptyParamsUriExtractor(@Nonnull String baseUri) {
      super(baseUri);
    }

    @Nonnull
    @Override
    public URI extract(@Nullable Object[] arguments) {
      try {
        return new URI(baseUrl);
      } catch (URISyntaxException e) {
        throw new IllegalStateException(e);
      }
    }
  }

  private static final class StandardUriExtractor extends AbstractUriExtractor {
    final PosNamePair[] variables;

    public StandardUriExtractor(@Nonnull String baseUri, @Nonnull PosNamePair[] variables) {
      super(baseUri);
      this.variables = variables;
    }

    @Nonnull
    @Override
    public URI extract(@Nullable Object[] arguments) {
      final UriTemplate uriTemplate = new UriTemplate(baseUrl);

      if (variables.length == 0) {
        return uriTemplate.expand();
      }

      if (arguments == null || arguments.length != variables.length) {
        // should never happen
        throw new IllegalStateException("Unexpected: too few arguments given");
      }

      final Map<String, Object> params = new HashMap<>(variables.length * 2);
      for (final PosNamePair pair : variables) {
        params.put(pair.argName, arguments[pair.argPos]);
      }

      return uriTemplate.expand(params);
    }
  }

  private static final class PosNamePair {
    final int argPos;
    final String argName;

    public PosNamePair(int argPos, String argName) {
      this.argPos = argPos;
      this.argName = argName;
    }
  }
}