Package com.alexkasko.springjdbc.typedqueries.codegen

Source Code of com.alexkasko.springjdbc.typedqueries.codegen.CodeGenerator

package com.alexkasko.springjdbc.typedqueries.codegen;

import freemarker.ext.beans.BeansWrapper;
import freemarker.template.Configuration;
import freemarker.template.Template;
import freemarker.template.TemplateException;

import java.io.*;
import java.math.BigDecimal;
import java.nio.charset.Charset;
import java.util.*;
import java.util.regex.Pattern;

import static freemarker.ext.beans.BeansWrapper.EXPOSE_PROPERTIES_ONLY;
import static freemarker.template.Configuration.SQUARE_BRACKET_TAG_SYNTAX;
import static java.util.Collections.unmodifiableMap;
import static org.springframework.jdbc.core.namedparam.NamedParamsSqlParser$Accessor.parseParamsNames;

/**
* Queries wrappers code generator. Uses FreeMarker template for generating wrapper class.
*
* @author alexkasko
* Date: 12/22/12
*/
public class CodeGenerator {

    private final boolean isPublic;
    private final boolean useIterableJdbcTemplate;
    private final boolean useCheckSingleRowUpdates;
    private final boolean useBatchInserts;
    private final boolean useTemplateStringSubstitution;
    private final Pattern selectRegex;
    private final Pattern updateRegex;
    private final Pattern templateRegex;
    private final Pattern templateValueConstraintRegex;
    private final Map<String, Class<?>> typeIdMap;
    private final String freemarkerTemplate;
    private final Configuration freemarkerConf;

    /**
     * Constructor
     *
     *
     * @param isPublic whether generated class and its methods will have 'public' access modifier
     * @param useIterableJdbcTemplate whether to use iterable jdbc template extensions from this
     *                                project (https://github.com/alexkasko/springjdbc-iterable)
     * @param useCheckSingleRowUpdates whether to generate additional update methods, those check that
*                                 only single row was changed on update
     * @param useBatchInserts whether to generate additional insert (DML) methods (with parameters), those
*                        takes {@link java.util.Iterator} of parameters and execute inserts
*                        for the contents of the specified iterator in batch mode
     * @param useTemplateStringSubstitution whether to recognize query templates on method generation
     * @param selectRegex regular expression to use for identifying 'select' queries by name
     * @param updateRegex regular expression to use for identifying 'insert', 'update' and 'delete' queries by name
     * @param templateRegex regular expression to recognize query templates by name
     * @param templateValueConstraintRegex regular expression constraint for template substitution values
     * @param typeIdMap mapping of parameter names postfixes to data types
     * @param freemarkerTemplate freemarker template body
     * @param freemarkerConf freemarker configuration    @throws CodeGeneratorException on any error
     */
    public CodeGenerator(boolean isPublic, boolean useIterableJdbcTemplate, boolean useCheckSingleRowUpdates,
                         boolean useBatchInserts, boolean useTemplateStringSubstitution, String selectRegex,
                         String updateRegex, String templateRegex, String templateValueConstraintRegex,
                         Map<String, Class<?>> typeIdMap, String freemarkerTemplate,
                         Configuration freemarkerConf) throws CodeGeneratorException {
        if(null == selectRegex) throw new CodeGeneratorException("Provided selectRegex is null");
        if(null == updateRegex) throw new CodeGeneratorException("Provided updateRegex is null");
        if(null == templateRegex) throw new CodeGeneratorException("Provided templateRegex is null");
        if(null == templateValueConstraintRegex) throw new CodeGeneratorException("Provided templateValueConstraintRegex is null");
        if(null == typeIdMap) throw new CodeGeneratorException("Provided typeIdMap is null");
        if(null == freemarkerTemplate) throw new CodeGeneratorException("Provided freemarkerTemplate is null");
        if(null == freemarkerConf) throw new CodeGeneratorException("Provided freemarkerConf is null");
        this.isPublic = isPublic;
        this.useIterableJdbcTemplate = useIterableJdbcTemplate;
        this.useCheckSingleRowUpdates = useCheckSingleRowUpdates;
        this.useBatchInserts = useBatchInserts;;
        this.useTemplateStringSubstitution = useTemplateStringSubstitution;
        this.selectRegex = Pattern.compile(selectRegex);
        this.updateRegex = Pattern.compile(updateRegex);
        this.templateRegex = Pattern.compile(templateRegex);
        this.templateValueConstraintRegex = Pattern.compile(templateValueConstraintRegex);
        this.typeIdMap = typeIdMap;
        this.freemarkerTemplate = freemarkerTemplate;
        this.freemarkerConf = freemarkerConf;
    }

    /**
     * Generates queries wrappers class
     *
     * @param queries name -> text query mapping
     * @param fullClassName class name with package prefix
     * @param sourceSqlFileName name of source SQL file
     * @param output output writer
     * @throws CodeGeneratorException
     */
    public void generate(Map<String, String> queries, String fullClassName, String sourceSqlFileName, Writer output)
            throws CodeGeneratorException {
        if(null == queries || 0 == queries.size()) throw new CodeGeneratorException("Provided queries map is empty");
        if(null == fullClassName || 0 == fullClassName.length()) throw new CodeGeneratorException("Provided fullClassName is empty");
        if(!fullClassName.contains(".") || (fullClassName.lastIndexOf(".") ==  fullClassName.length())) throw new CodeGeneratorException(
                "Full class name must have non empty package prefix, but was: [" + fullClassName + "]");
        if(null == output) throw new CodeGeneratorException("Provided output is null");
        try {
            RootTemplateArg params = createTemplateArgs(queries, fullClassName, sourceSqlFileName);
            Reader templateReader = new StringReader(freemarkerTemplate);
            Template ftl = new Template(fullClassName, templateReader, freemarkerConf, "UTF-8");
            ftl.process(params, output);
        } catch (TemplateException e) {
            throw new CodeGeneratorException("Error processing FreeMarker template", e);
        } catch (IOException e) {
            throw new CodeGeneratorException("IO error", e);
        } catch (Exception e) {
            if(e instanceof CodeGeneratorException) throw (CodeGeneratorException) e;
            throw new CodeGeneratorException("Error generating queries code", e);
        }
    }

    private RootTemplateArg createTemplateArgs(Map<String, String> queries, String fullClassName, String sourceSqlFileName) {
        int dotIndex = fullClassName.lastIndexOf(".");
        String packageName = fullClassName.substring(0, dotIndex);
        String className = fullClassName.substring(dotIndex + 1);
        String modifier = isPublic ? "public" : "";
        List<QueryTemplateArg> selects = new ArrayList<QueryTemplateArg>();
        List<QueryTemplateArg> updates = new ArrayList<QueryTemplateArg>();
        for (Map.Entry<String, String> en : queries.entrySet()) {
            String name = en.getKey();
            String sql = en.getValue();
            Set<ParamTemplateArg> params = createNamedParams(sql);
            boolean isTemplate = useTemplateStringSubstitution && templateRegex.matcher(name).matches();
            QueryTemplateArg query = new QueryTemplateArg(name, params, isTemplate);
            if (selectRegex.matcher(name).matches()) selects.add(query);
            else if (updateRegex.matcher(name).matches()) updates.add(query);
            else throw new CodeGeneratorException("Invalid query name: [" + name + "], names must match select regex: " +
                        "[" + selectRegex + "] or updateRegex: [" + updateRegex + "]");
        }
        return new RootTemplateArg(packageName, className, modifier, useIterableJdbcTemplate, useCheckSingleRowUpdates,
                useBatchInserts, useTemplateStringSubstitution, sourceSqlFileName, templateValueConstraintRegex.pattern(),
                selects, updates);
    }

    private Set<ParamTemplateArg> createNamedParams(String sql) {
        List<String> paramNames = parseParamsNames(sql);
        Set<ParamTemplateArg> args = new LinkedHashSet<ParamTemplateArg>(paramNames.size());
        for (String name : paramNames) {
            args.add(new ParamTemplateArg(name, typeForName(name)));
        }
        return args;
    }

    private Class typeForName(String name) {
        for (Map.Entry<String, Class<?>> en : typeIdMap.entrySet()) {
            String postfix = en.getKey();
            if (name.endsWith(postfix)) return en.getValue();
        }
        return Object.class;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public String toString() {
        final StringBuilder sb = new StringBuilder();
        sb.append("CodeGenerator");
        sb.append("{isPublic=").append(isPublic);
        sb.append(", useIterableJdbcTemplate=").append(useIterableJdbcTemplate);
        sb.append(", selectRegex=").append(selectRegex);
        sb.append(", updateRegex=").append(updateRegex);
        sb.append(", typeIdMap=").append(typeIdMap);
        sb.append('}');
        return sb.toString();
    }

    /**
     * Creates builder instance
     *
     * @return builder instance
     */
    public static Builder builder() {
        return new Builder();
    }

    /**
     * Builder class for configuring and instantiating {@link CodeGenerator}
     */
    public static class Builder {
        private boolean isPublic = false;
        private boolean useIterableJdbcTemplate = false;
        private boolean useCheckSingleRowUpdates = false;
        private boolean useBatchInserts = false;
        private boolean useTemplateStringSubstitution = false;
        private String selectRegex = "^select[a-zA-Z][a-zA-Z0-9_$]*$";
        private String updateRegex = "^(?:insert|update|delete|create|drop)[a-zA-Z][a-zA-Z0-9_$]*$";
        private String templateRegex = "^[a-zA-Z0-9_$]*Template$";
        private String templateValueConstraintRegex = "^[a-zA-Z0-9_$]*$";
        private Map<String, Class<?>> typeIdMap = defaultTypeIdMap();
        private String freemarkerTemplate = defaultFreemarkerTemplate();
        private Configuration freemarkerConf = defaultFreemarkerConf();

        /**
         * Public access modifier setter, false by default
         *
         * @param aPublic whether generated class and its methods will have 'public' access modifier
         * @return builder itself
         */
        public Builder setPublic(boolean aPublic) {
            isPublic = aPublic;
            return this;
        }

        /**
         * Whether to use iterable JDBC extensions (https://github.com/alexkasko/springjdbc-iterable),
         * false by default
         *
         * @param useIterableJdbcTemplate whether to use iterable jdbc template extensions
         * @return builder itself
         */
        public Builder setUseIterableJdbcTemplate(boolean useIterableJdbcTemplate) {
            this.useIterableJdbcTemplate = useIterableJdbcTemplate;
            return this;
        }

        /**
         * Whether to generate additional update methods, those check that
         * only single row was changed on update
         *
         * @param useCheckSingleRowUpdates whether to generate additional update methods, those check that
         *                                 only single row was changed on update
         * @return builder itself
         */
        public Builder setUseCheckSingleRowUpdates(boolean useCheckSingleRowUpdates) {
            this.useCheckSingleRowUpdates = useCheckSingleRowUpdates;
            return this;
        }

        /**
         * Whether to generate additional insert (DML) methods (with parameters), those
         * takes {@link java.util.Iterator} of parameters and execute inserts
         *
         * @param useBatchInserts whether to generate additional insert (DML) methods (with parameters), those
         *                        takes {@link java.util.Iterator} of parameters and execute inserts
         * @return builder itself
         */
        public Builder setUseBatchInserts(boolean useBatchInserts) {
            this.useBatchInserts = useBatchInserts;
            return this;
        }

        /**
         * Whether to recognize query templates on method generation
         *
         * @param useTemplateStringSubstitution whether to recognize query templates on method generation
         * @return builder itself
         */
        public Builder setUseTemplateStringSubstitution(boolean useTemplateStringSubstitution) {
            this.useTemplateStringSubstitution = useTemplateStringSubstitution;
            return this;
        }

        /**
         * Regular expression to use for identifying 'select' queries by name,
         * default: {@code ^select[a-zA-Z][a-zA-Z0-9_$]*$}
         *
         * @param selectRegex regular expression to use for identifying 'select' queries by name
         * @return builder itself
         */
        public Builder setSelectRegex(String selectRegex) {
            this.selectRegex = selectRegex;
            return this;
        }

        /**
         * Regular expression to use for identifying 'insert', 'update' and 'delete' queries by name,
         * default: {@code ^(?:insert|update|delete|create|drop)[a-zA-Z][a-zA-Z0-9_$]*$}
         *
         * @param updateRegex regular expression to use for identifying 'insert',
         *                    'update' and 'delete' queries by name
         * @return builder itself
         */
        public Builder setUpdateRegex(String updateRegex) {
            this.updateRegex = updateRegex;
            return this;
        }

        /**
         * Mapping of parameter names postfixes to data types, default:
         * <pre>{@code
         * {
         *      "String": "java.lang.String",
         *      "Text": "java.lang.String",
         *      "_text": "java.lang.String",
         *      "Name": "java.lang.String",
         *      "_name": "java.lang.String",
         *      "Bool": "boolean"",
         *      "_bool": "boolean"",
         *      "Short": "short",
         *      "_short": "short",
         *      "Int": "int",
         *      "_int": "int",
         *      "Long": "long",
         *      "_long": "long",
         *      "Id": "long",
         *      "_id": "long",
         *      "Number": "long",
         *      "_number": "long",
         *      "Count": "long",
         *      "_count": "long",
         *      "Size": "long",
         *      "_size": "long",
         *      "Float": "float",
         *      "_float": "float",
         *      "Double": "double",
         *      "_double": "double",
         *      "Numeric": "java.math.BigDecimal",
         *      "_numeric": "java.math.BigDecimal",
         *      "Binary": "byte[]",
         *      "_binary": "byte[]",
         *      "Date": "java.util.Date",
         *      "_date": "java.util.Date",
         *      "List": "java.util.Collection",
         *      "_list": "java.util.Collection",
         *      "Collection": "java.util.Collection",
         *      "_collection": "java.util.Collection",
         *      "Set": "java.util.Collection",
         *      "_set": "java.util.Collection"
         * }
         * }</pre>
         *
         * @param typeIdMap postfix->type mapping
         * @return builder itself
         */
        public Builder setTypeIdMap(Map<String, Class<?>> typeIdMap) {
            this.typeIdMap = typeIdMap;
            return this;
        }

        /**
         * Regular expression to recognize query templates by name,
         * default value: {@code ^[a-zA-Z0-9_$]*Template$}
         *
         * @param templateRegex template recognition regex
         * @return builder itself
         */
        public Builder setTemplateRegex(String templateRegex) {
            this.templateRegex = templateRegex;
            return this;
        }

        /**
         * Regular expression constraint for template substitution values,
         * default value: {@code ^[a-zA-Z0-9_$]*$}
         *
         * @param templateValueConstraintRegex regular expression constraint for template substitution values
         * @return builder itself
         */
        public Builder setTemplateValueConstraintRegex(String templateValueConstraintRegex) {
            this.templateValueConstraintRegex = templateValueConstraintRegex;
            return this;
        }

        /**
         * FreeMarker template body for queries class, loaded from classpath
         * {@code /com/alexkasko/springjdbc/typedqueries/codegen/BeanQueries.ftl} by default
         *
         * @param freemarkerTemplate
         * @return builder itself
         */
        public Builder setFreemarkerTemplate(String freemarkerTemplate) {
            this.freemarkerTemplate = freemarkerTemplate;
            return this;
        }

        /**
         * Freemarker configuration instance, default:
         * <pre>
         * {@code
         *   Configuration fc = new Configuration();
         *   fc.setLocalizedLookup(false);
         *   fc.setTagSyntax(SQUARE_BRACKET_TAG_SYNTAX);
         *   fc.setTemplateUpdateDelay(Integer.MAX_VALUE);
         *   fc.setNumberFormat("computer");
         *   BeansWrapper bw = (BeansWrapper) fc.getObjectWrapper();
         *   bw.setExposureLevel(EXPOSE_PROPERTIES_ONLY);
         *   return fc;
         * }
         * </pre>
         *
         * @param freemarkerConf freemarker configuration instance
         * @return builder itself
         */
        public Builder setFreemarkerConf(Configuration freemarkerConf) {
            this.freemarkerConf = freemarkerConf;
            return this;
        }

        /**
         * Creates configured {@link CodeGenerator} instance
         *
         * @return configured {@link CodeGenerator} instance
         */
        public CodeGenerator build() {
            return new CodeGenerator(isPublic, useIterableJdbcTemplate, useCheckSingleRowUpdates, useBatchInserts,
                    useTemplateStringSubstitution, selectRegex, updateRegex, templateRegex, templateValueConstraintRegex,
                    typeIdMap, freemarkerTemplate, freemarkerConf);
        }

        private static Map<String, Class<?>> defaultTypeIdMap() {
            Map<String, Class<?>> map = new LinkedHashMap<String, Class<?>>();
            map.put("String", String.class);
            map.put("_string", String.class);
            map.put("Text", String.class);
            map.put("_text", String.class);
            map.put("Name", String.class);
            map.put("_name", String.class);
            map.put("Bool", boolean.class);
            map.put("_bool", boolean.class);
            map.put("Short", short.class);
            map.put("_short", short.class);
            map.put("Int", int.class);
            map.put("_int", int.class);
            map.put("Long", long.class);
            map.put("_long", long.class);
            map.put("Id", long.class);
            map.put("_id", long.class);
            map.put("Number", long.class);
            map.put("_number", long.class);
            map.put("Count", long.class);
            map.put("_count", long.class);
            map.put("Size", long.class);
            map.put("_size", long.class);
            map.put("Float", float.class);
            map.put("_float", float.class);
            map.put("Double", double.class);
            map.put("_double", double.class);
            map.put("Numeric", BigDecimal.class);
            map.put("_numeric", BigDecimal.class);
            map.put("Binary", byte[].class);
            map.put("_binary", byte[].class);
            map.put("Date", Date.class);
            map.put("_date", Date.class);
            map.put("List", Collection.class);
            map.put("_list", Collection.class);
            map.put("Collection", Collection.class);
            map.put("_collection", Collection.class);
            map.put("Set", Collection.class);
            map.put("_set", Collection.class);
            return unmodifiableMap(map);
        }

        private static Configuration defaultFreemarkerConf() {
            Configuration fc = new Configuration();
            fc.setLocalizedLookup(false);
            fc.setTagSyntax(SQUARE_BRACKET_TAG_SYNTAX);
            fc.setTemplateUpdateDelay(Integer.MAX_VALUE);
            fc.setNumberFormat("computer");
            BeansWrapper bw = (BeansWrapper) fc.getObjectWrapper();
            bw.setExposureLevel(EXPOSE_PROPERTIES_ONLY);
            return fc;
        }

        private static String defaultFreemarkerTemplate() {
            InputStream is = null;
            try {
                is = CodeGenerator.class.getResourceAsStream("/com/alexkasko/springjdbc/typedqueries/codegen/BeanQueries.ftl");
                byte[] data = readFull(is);
                return new String(data, Charset.forName("UTF-8"));
            } catch (IOException e) {
                throw new RuntimeException(e);
            } finally {
                closeQuietly(is);
            }
        }

        private static byte[] readFull(InputStream is) throws IOException {
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            byte[] buffer = new byte[1024 * 4];
            int n;
            while (-1 != (n = is.read(buffer))) {
                baos.write(buffer, 0, n);
            }
            return baos.toByteArray();
        }

        private static void closeQuietly(InputStream is) {
            try {
                if (is != null) {
                    is.close();
                }
            } catch (IOException ioe) {
                // ignore
            }
        }
    }
}
TOP

Related Classes of com.alexkasko.springjdbc.typedqueries.codegen.CodeGenerator

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.