com.mastfrog.parameters.processor.Processor.java Source code

Java tutorial

Introduction

Here is the source code for com.mastfrog.parameters.processor.Processor.java

Source

/*
 * The MIT License
 *
 * Copyright 2014 Tim Boudreau.
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */
package com.mastfrog.parameters.processor;

import com.mastfrog.parameters.Param;
import com.mastfrog.parameters.Params;
import java.io.IOException;
import java.io.OutputStream;
import java.lang.annotation.AnnotationTypeMismatchException;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.Filer;
import javax.annotation.processing.FilerException;
import javax.annotation.processing.RoundEnvironment;
import javax.annotation.processing.SupportedAnnotationTypes;
import javax.annotation.processing.SupportedSourceVersion;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.AnnotationMirror;
import javax.lang.model.element.AnnotationValue;
import javax.lang.model.element.Element;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.PackageElement;
import javax.lang.model.element.TypeElement;
import javax.lang.model.element.VariableElement;
import javax.lang.model.type.DeclaredType;
import javax.lang.model.type.TypeMirror;
import javax.lang.model.util.Elements;
import javax.lang.model.util.Types;
import javax.tools.Diagnostic;
import javax.tools.FileObject;
import javax.tools.JavaFileObject;
import javax.tools.StandardLocation;
import org.netbeans.validation.api.Problems;
import org.netbeans.validation.api.builtin.stringvalidation.StringValidators;
import org.openide.util.lookup.ServiceProvider;

/**
 * Annotation processor which generates typesafe classes for parameters
 *
 * @author Tim Boudreau
 */
@SupportedAnnotationTypes("com.mastfrog.parameters.Params")
@SupportedSourceVersion(SourceVersion.RELEASE_7)
@ServiceProvider(service = javax.annotation.processing.Processor.class)
public final class Processor extends AbstractProcessor {

    @Override
    public Set<String> getSupportedAnnotationTypes() {
        return Collections.singleton("com.mastfrog.parameters.Params");
    }

    private String optionalType = "java.util.Optional";
    private String fromNullable = "ofNullable";

    @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment re) {

        SourceVersion sv = processingEnv.getSourceVersion();
        if (sv.ordinal() > SourceVersion.RELEASE_7.ordinal()) {
            optionalType = "java.util.Optional";
        } else {
            TypeElement el = processingEnv.getElementUtils().getTypeElement(optionalType);
            if (el == null) {
                optionalType = "com.mastfrog.util.Optional";
            } else {
                optionalType = "com.google.common.base.Optional";
                fromNullable = "fromNullable";
            }
        }

        Set<? extends Element> all = re.getElementsAnnotatedWith(Params.class);
        List<GeneratedParamsClass> interfaces = new LinkedList<>();
        outer: for (Element e : all) {
            TypeElement te = (TypeElement) e;
            if (te.getSimpleName().toString().endsWith("__GenPage")) {
                continue;
            }
            PackageElement pkg = findPackage(e);
            if (pkg == null) {
                processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR,
                        "@Params may not be used in the default package", e);
                continue;
            }
            if (!isPageSubtype(te)) {
                processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR,
                        "@Params must be used on a subclass of org.apache.wicket.Page", e);
                continue;
            }
            String className = te.getQualifiedName().toString();
            Params params = te.getAnnotation(Params.class);

            Map<String, List<String>> validators = validatorsForParam(e);
            GeneratedParamsClass inf = new GeneratedParamsClass(className, te, pkg, params, validators);

            if (!params.useRequestBody()) {
                checkConstructor(te, inf);
            }
            interfaces.add(inf);
            Set<String> names = new HashSet<>();
            for (Param param : params.value()) {
                //                if (param.required() && !param.defaultValue().isEmpty()) {
                //                    processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "Don't set required to "
                //                            + "true if you are providing a default value - required makes it an error not "
                //                            + "to have a value, and if there is a default value, that error is an impossibility "
                //                            + "because it will always have a value.", e);
                //                    continue outer;
                //                }
                if (param.value().trim().isEmpty()) {
                    processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "Empty parameter name", e);
                    continue outer;
                }
                if (!isJavaIdentifier(param.value())) {
                    processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR,
                            "Not a valid Java identifier: " + param.value(), e);
                    continue outer;
                }
                if (!names.add(param.value())) {
                    processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR,
                            "Duplicate parameter name '" + param.value() + "'", e);
                    continue outer;
                }
                for (char c : ";,./*!@&^/\\<>?'\"[]{}-=+)(".toCharArray()) {
                    if (param.value().contains("" + c)) {
                        processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR,
                                "Param name may not contain the character '" + c + "'", e);
                    }
                }
                inf.add(param);
            }
        }
        Filer filer = processingEnv.getFiler();
        StringBuilder listBuilder = new StringBuilder();
        for (GeneratedParamsClass inf : interfaces) {
            try {
                String pth = inf.packageAsPath() + '/' + inf.className;
                pth = pth.replace('/', '.');
                JavaFileObject obj = filer.createSourceFile(pth, inf.el);
                try (OutputStream out = obj.openOutputStream()) {
                    out.write(inf.toString().getBytes("UTF-8"));
                }
                listBuilder.append(inf.className).append('\n');
            } catch (Exception ex) {
                ex.printStackTrace();
                processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR,
                        "Error processing annotation: " + ex.getMessage(), inf.el);
                Logger.getLogger(Processor.class.getName()).log(Level.SEVERE, null, ex);
            }
        }
        if (re.processingOver()) {
            try {
                FileObject list = filer.createResource(StandardLocation.CLASS_OUTPUT, "",
                        "META-INF/paramanos/bind.list", all.toArray(new Element[0]));
                try (OutputStream out = list.openOutputStream()) {
                    out.write(listBuilder.toString().getBytes("UTF-8"));
                }
            } catch (FilerException ex) {
                Logger.getLogger(Processor.class.getName()).log(Level.INFO, null, ex);
            } catch (IOException ex) {
                Logger.getLogger(Processor.class.getName()).log(Level.SEVERE, null, ex);
            }
        }
        return true;
    }

    private AnnotationMirror findMirror(Element el) {
        for (AnnotationMirror mir : el.getAnnotationMirrors()) {
            TypeMirror type = mir.getAnnotationType().asElement().asType();
            if (Params.class.getName().equals(type.toString())) {
                return mir;
            }
        }
        return null;
    }

    private Map<String, List<String>> validatorsForParam(Element el) {
        AnnotationMirror mirror = findMirror(el);
        Map<String, List<String>> result = new HashMap<>();
        List<AnnotationMirror> params = findParamAnnotations(mirror, new LinkedList<AnnotationMirror>());
        for (AnnotationMirror m : params) {
            List<String> names = findValidatorClassNames(m);
            if (!names.isEmpty()) {
                String name = findParamName(m);
                if (name != null) {
                    result.put(name, names);
                }
            }
        }
        return result;
    }

    private String findParamName(AnnotationMirror mir) {
        String result = null;
        for (Map.Entry<? extends ExecutableElement, ? extends AnnotationValue> e : mir.getElementValues()
                .entrySet()) {
            if (e.getKey().getSimpleName().contentEquals("value")) {
                if (e.getValue().getValue() instanceof String) {
                    result = (String) e.getValue().getValue();
                    break;
                }
            }
        }
        return result;
    }

    private List<AnnotationMirror> findParamAnnotations(AnnotationMirror mir, List<AnnotationMirror> result) {
        for (Map.Entry<? extends ExecutableElement, ? extends AnnotationValue> e : mir.getElementValues()
                .entrySet()) {
            if ("value()".equals(e.getKey().toString())) {
                if (e.getValue().getValue() instanceof List) {
                    List<?> l = (List<?>) e.getValue().getValue();
                    for (Object o : l) {
                        if (o instanceof AnnotationMirror) {
                            AnnotationMirror param = (AnnotationMirror) o;
                            result.add(param);
                            findValidatorClassNames(param);
                        }
                    }
                }
            }
        }
        return result;
    }

    private List<String> findValidatorClassNames(AnnotationMirror mir) {
        List<String> result = new LinkedList<String>();
        for (Map.Entry<? extends ExecutableElement, ? extends AnnotationValue> e : mir.getElementValues()
                .entrySet()) {
            if ("validators()".equals(e.getKey().toString())) {
                if (e.getValue().getValue() instanceof List) {
                    List<?> l = (List<?>) e.getValue().getValue();
                    for (Object o : l) {
                        String s = o.toString();
                        if (s.endsWith(".class")) {
                            s = s.substring(0, s.length() - ".class".length());
                        }
                        result.add(s);
                    }
                } else {
                    result.add(e.getValue().getValue().toString());
                }
            }
        }
        return result;
    }

    static boolean isJavaIdentifier(String id) {
        if (id == null) {
            return false;
        }
        return SourceVersion.isIdentifier(id) && !SourceVersion.isKeyword(id);
    }

    private final PackageElement findPackage(Element e) {
        for (Element curr = e; e != null;) {
            if (curr instanceof PackageElement) {
                return (PackageElement) curr;
            } else {
                curr = curr.getEnclosingElement();
            }
        }
        return null;
    }

    private boolean isPageSubtype(TypeElement e) {
        if (true) {
            return true;
        }
        Types types = processingEnv.getTypeUtils();
        Elements elements = processingEnv.getElementUtils();
        TypeElement pageType = elements.getTypeElement("org.apache.wicket.Page");
        if (pageType == null) {
            processingEnv.getMessager().printMessage(Diagnostic.Kind.WARNING,
                    "org.apache.wicket.Page not on classpath, cannot process annotation");
            return false;
        }
        return types.isSubtype(e.asType(), pageType.asType());
    }

    private void checkConstructor(TypeElement el, GeneratedParamsClass inf) {
        Elements elements = processingEnv.getElementUtils();
        TypeElement pageParamsType = elements
                .getTypeElement("org.apache.wicket.request.mapper.parameter.PageParameters");
        TypeElement customParamsType = elements.getTypeElement(inf.qualifiedName());
        boolean found = false;
        boolean foundArgument = false;
        ExecutableElement con = null;
        outer: for (Element sub : el.getEnclosedElements()) {
            switch (sub.getKind()) {
            case CONSTRUCTOR:
                for (AnnotationMirror mir : sub.getAnnotationMirrors()) {
                    DeclaredType type = mir.getAnnotationType();
                    switch (type.toString()) {
                    case "javax.inject.Inject":
                    case "com.google.inject.Inject":
                        ExecutableElement constructor = (ExecutableElement) sub;
                        con = constructor;
                        for (VariableElement va : constructor.getParameters()) {
                            TypeMirror varType = va.asType();
                            if (pageParamsType != null && varType.toString().equals(pageParamsType.toString())) {
                                foundArgument = true;
                                break;
                            }
                            if (customParamsType != null
                                    && varType.toString().equals(customParamsType.toString())) {
                                foundArgument = true;
                                break;
                            } else if (customParamsType == null && inf.qualifiedName().equals(varType.toString())) { //first compilation - type not generated yet
                                foundArgument = true;
                                break;
                            }
                        }
                        found = true;
                        break outer;
                    default:
                        //do nothing
                    }
                }
            }
        }
        if (!found) {
            processingEnv.getMessager().printMessage(Diagnostic.Kind.WARNING, "Usually a constructor "
                    + "annotated with @Inject that takes a " + inf.className + " is desired", el);
        } else if (found && !foundArgument) {
            processingEnv.getMessager().printMessage(Diagnostic.Kind.WARNING,
                    "Usually a constructor taking " + "an argument of " + inf.className + " is desired", con);
        }
    }

    private final class GeneratedParamsClass {

        private final List<GeneratedParameter> methods = new LinkedList<>();
        private final String packageName;
        private final String className;
        private final String srcClassName;
        private final TypeElement el;
        private final boolean jsonConstructor;
        private final boolean anySetter;
        private final boolean validate;
        private final boolean generateToMap = true;
        private final Params params;
        private final Map<String, List<String>> validators;

        GeneratedParamsClass(String className, TypeElement el, PackageElement pkg, Params params,
                Map<String, List<String>> validators) {
            this.params = params;
            this.el = el;
            srcClassName = className;
            this.packageName = pkg.getQualifiedName().toString();
            this.className = className.substring(className.lastIndexOf('.') + 1) + "Params";
            this.jsonConstructor = params.jsonConstructor();
            this.anySetter = params.allowUnlistedParameters();
            this.validate = params.generateValidationCode();
            this.validators = validators;
        }

        public String qualifiedName() {
            return packageName + "." + className;
        }

        public String packageAsPath() {
            return packageName.replace('.', '/');
        }

        public void add(Param param) {
            methods.add(new GeneratedParameter(param));
        }

        private boolean needOptional() {
            for (GeneratedParameter m : methods) {
                if (!m.isRequired()) {
                    return true;
                }
            }
            return params.allowUnlistedParameters();
        }

        private Set<String> stringValidators() {
            Set<String> result = new HashSet<>();
            for (Param param : params.value()) {
                for (StringValidators v : param.constraints()) {
                    result.add(v.name());
                }
            }
            return result;
        }

        @Override
        public String toString() {
            StringBuilder sb = new StringBuilder(200);
            try {
                sb.append("package ").append(packageName).append(";\n\n");
                List<String> imports = new LinkedList<>();
                List<String> interfaces = new LinkedList<>();
                interfaces.add("Serializable");
                if (anySetter && jsonConstructor) {
                    imports.add("com.fasterxml.jackson.annotation.JsonAnySetter");
                }
                if (jsonConstructor) {
                    imports.add("com.fasterxml.jackson.annotation.JsonCreator");
                    imports.add("com.fasterxml.jackson.annotation.JsonProperty");
                }
                if (params.generateToJSON()) {
                    imports.add("com.fasterxml.jackson.databind.ObjectMapper");
                    imports.add("com.fasterxml.jackson.core.JsonProcessingException");
                }
                if (validate) {
                    imports.add("org.netbeans.validation.api.Validator");
                    imports.add("org.netbeans.validation.api.Problems");
                    imports.add("com.google.inject.Injector");
                    imports.add("com.mastfrog.parameters.gen.Validatable");
                    interfaces.add("Validatable");
                    for (String validator : stringValidators()) {
                        imports.add("static org.netbeans.validation.api.builtin.stringvalidation.StringValidators."
                                + validator);
                    }
                }
                imports.add("com.mastfrog.parameters.KeysValues");
                imports.add("com.mastfrog.parameters.gen.Origin");
                imports.add("java.io.Serializable");
                if (anySetter || generateToMap) {
                    imports.add("java.util.Map");
                    imports.add("java.util.HashMap");
                }
                imports.add("java.util.Objects");
                if (needOptional()) {
                    imports.add(optionalType);
                }
                imports.add("javax.inject.Inject");
                Collections.sort(imports);
                for (String s : imports) {
                    sb.append("import " + s + ";\n");
                }
                sb.append("/** \n    Generated from &#064;Param annotations on ").append(srcClassName)
                        .append("\n*/\n");
                Collections.sort(methods);
                sb.append("@Origin(").append(srcClassName).append(".class)\n");
                if (jsonConstructor) {
                    sb.append("public ");
                }
                sb.append("final class ").append(className).append(" ");
                if (!interfaces.isEmpty()) {
                    sb.append("implements ");
                    for (Iterator<String> it = interfaces.iterator(); it.hasNext();) {
                        String iface = it.next();
                        sb.append(iface);
                        if (it.hasNext()) {
                            sb.append(", ");
                        }
                    }
                }
                sb.append(" {\n");
                for (GeneratedParameter m : methods) {
                    indent(m.varDeclaration(), sb, 1);
                }
                if (anySetter) {
                    indent("private final Map<String,String> __metadata = new HashMap<>();", sb, 1);
                }
                sb.append('\n');
                indent("@Inject", sb, 1);
                indent("public " + className + " (KeysValues params) {", sb, 1);
                for (GeneratedParameter m : methods) {
                    indent("this." + m.fieldName() + " = " + m.loadClause(), sb, 2);
                }
                if (anySetter) {
                    indent("for (Map.Entry<String,String> __e : params) {", sb, 2);
                    indent("switch (__e.getKey()) {", sb, 3);
                    for (GeneratedParameter m : methods) {
                        indent("case \"" + m.param.value() + "\" :", sb, 4);
                    }
                    indent("break;", sb, 5);
                    indent("default :", sb, 4);
                    indent("__any (__e.getKey(), __e.getValue());", sb, 5);
                    indent("}", sb, 3);
                    indent("}", sb, 2);
                }
                indent("}\n", sb, 1);

                if (jsonConstructor) {
                    indent("@JsonCreator", sb, 1);
                    indent("public " + className + "(", sb, 1);
                    for (Iterator<GeneratedParameter> it = methods.iterator(); it.hasNext();) {
                        GeneratedParameter m = it.next();
                        StringBuilder b = new StringBuilder();
                        b.append("@JsonProperty(");
                        if (m.isRequired()) {
                            b.append("value=\"").append(m.param.value()).append("\"");
                        } else {
                            b.append("value=\"").append(m.param.value()).append("\"").append(", required=false");
                        }
                        b.append(") ");
                        try {
                            b.append(m.param.type().typeName(m.isRequired())).append(" ");
                        } catch (EnumConstantNotPresentException e) {
                            b.append("INVALID_ANNOTATION").append(" ");
                        }
                        b.append(m.fieldName());
                        if (it.hasNext()) {
                            b.append(",");
                        } else {
                            b.append(") {");
                        }
                        indent(b.toString(), sb, 2);
                    }
                    for (GeneratedParameter m : methods) {
                        String defVal = null;
                        if (!"".equals(m.param.defaultValue())) {
                            try {
                                if (m.param.type().isString()) {
                                    defVal = '"' + m.param.defaultValue().replaceAll("\"", "\\\"") + '"';
                                } else {
                                    defVal = m.param.defaultValue();
                                    switch (m.param.type()) {
                                    case LONG:
                                    case NON_NEGATIVE_LONG:
                                        if (!defVal.endsWith("L") && !defVal.endsWith("l")) {
                                            defVal += "L";
                                        }
                                    }
                                }
                            } catch (EnumConstantNotPresentException e) {
                                defVal = "null; // " + e.getMessage();
                            }
                        }
                        if (defVal != null) {
                            indent("this." + m.fieldName() + " = " + m.fieldName() + " == null ? " + defVal + " : "
                                    + m.fieldName() + ";", sb, 2);
                        } else if (!m.param.required()) {
                            indent("this." + m.fieldName() + " = Optional." + fromNullable + "(" + m.fieldName()
                                    + ");", sb, 2);
                        } else {
                            indent("this." + m.fieldName() + " = " + m.fieldName() + ";", sb, 2);
                        }
                    }
                    indent("}\n", sb, 1);
                }

                if (anySetter) {
                    sb.append("\n");
                    if (jsonConstructor) {
                        indent("@JsonAnySetter", sb, 1);
                    }
                    indent("public void __any(String key, String value){", sb, 1);
                    indent("__metadata.put(key, value);", sb, 2);
                    indent("}", sb, 1);
                    sb.append("\n");
                    indent("public Optional<String> get(String key) {", sb, 1);
                    indent("return Optional." + fromNullable + "(__metadata.get(key));", sb, 2);
                    indent("}", sb, 1);
                    sb.append("\n");
                }

                for (GeneratedParameter m : methods) {
                    indent(m.toString(), sb, 1);
                }
                indent("@Override", sb, 1);
                indent("public String toString() {", sb, 1);
                indent("return ", sb, 2);
                sb.append("           ");
                for (int i = 0; i < methods.size(); i++) {
                    GeneratedParameter m = methods.get(i);
                    sb.append("\" ").append(m.param.value()).append(" = ").append('"').append(" + ")
                            .append(m.fieldName());
                    if (i != methods.size() - 1) {
                        sb.append("\n            + ");
                    }
                }
                sb.append(";\n");
                indent("}\n", sb, 1);

                indent("@Override", sb, 1);
                indent("public boolean equals (Object o) {", sb, 1);
                indent("if (o == this) {", sb, 2);
                indent("return true;", sb, 3);
                indent("} else if (o == null) {", sb, 2);
                indent("return false;", sb, 3);
                indent("}\n", sb, 2);
                indent("if (o instanceof " + className + ") {", sb, 2);
                indent(className + " other = (" + className + ") o;", sb, 3);
                indent("return ", sb, 3);
                for (int i = 0; i < methods.size(); i++) {
                    GeneratedParameter method = methods.get(i);
                    String delim = i < methods.size() - 1 ? " &&" : ";";
                    if (method.isPrimitive()) {
                        indent("this." + method.fieldName() + " == other." + method.fieldName() + delim, sb, 4);
                    } else {
                        indent("Objects.equals(this." + method.fieldName() + ", other." + method.fieldName() + ") "
                                + delim, sb, 4);
                    }
                }
                indent("}", sb, 2);
                indent("return false;", sb, 2);
                indent("}\n", sb, 1);

                indent("@Override", sb, 1);
                indent("public int hashCode() {", sb, 1);
                indent("return Objects.hash(", sb, 2);
                for (int i = 0; i < methods.size(); i++) {
                    GeneratedParameter method = methods.get(i);
                    String delim = i < methods.size() - 1 ? "," : ");";
                    indent(method.fieldName() + delim, sb, 3);
                }
                indent("}", sb, 1);

                if (validate) {
                    sb.append("\n");
                    indent("@Override", sb, 1);
                    indent("public Problems validate (Injector inj, Problems problems) {", sb, 1);
                    for (GeneratedParameter p : methods) {
                        List<String> validatorTypes = validators.get(p.param.value());

                        if (p.param.constraints().length == 0
                                && (validatorTypes == null || validatorTypes.isEmpty())) {
                            continue;
                        }
                        final boolean optional = !p.isRequired() && "".equals(p.param.defaultValue());
                        if (p.param.constraints().length > 0) {
                            int ind = optional ? 2 : 3;
                            if (optional) {
                                indent("if (" + p.fieldName() + ".isPresent()) {", sb, 2);
                            }
                            for (StringValidators v : p.param.constraints()) {
                                indent(v.name() + ".validate(problems, \"" + p.param.value() + "\", "
                                        + p.fieldName() + (optional ? ".get()" : "") + ");", sb,
                                        optional ? ind + 1 : ind);
                            }
                            if (optional) {
                                indent("}", sb, 2);
                            }
                        }
                        int ix = 0;
                        if (validatorTypes != null && !validatorTypes.isEmpty()) {
                            for (String type : validatorTypes) {
                                String varName = p.fieldName() + "Validator" + ++ix;
                                if (!optional) {
                                    indent("Validator<String> " + varName + " = inj.getInstance(" + type
                                            + ".class);", sb, 2);
                                    indent(varName + ".validate (problems, " + "\"" + p.param.value() + "\", "
                                            + p.fieldName() + (p.param.type().isString() ? "" : " + \"\"") + ");",
                                            sb, 2);
                                } else {
                                    indent("if (" + p.fieldName() + ".isPresent()) {", sb, 2);
                                    indent("Validator<String>  " + varName + " = inj.getInstance(" + type
                                            + ".class);", sb, 3);
                                    indent(varName + ".validate (problems, " + "\"" + p.param.value() + "\", "
                                            + p.fieldName() + ".get() "
                                            + (p.param.type().isString() ? "" : " + \"\"") + ");", sb, 3);
                                    indent("}", sb, 2);
                                }
                            }
                        }
                    }
                    indent("return problems;", sb, 2);
                    indent("}", sb, 1);
                }
                if (generateToMap) {
                    sb.append("\n");
                    indent("public Map<String,Object> toMap() {", sb, 1);
                    indent("Map<String,Object> result = new HashMap<>();", sb, 2);
                    for (GeneratedParameter p : methods) {
                        boolean optional = p.varDeclaration().contains("Optional");
                        if (!optional) {
                            indent("result.put(\"" + p.param.value() + "\", " + p.fieldName() + ");", sb, 2);
                        } else {
                            indent("if (" + p.fieldName() + ".isPresent()) {", sb, 2);
                            indent("result.put(\"" + p.param.value() + "\", " + p.fieldName() + ".get());", sb, 3);
                            indent("}", sb, 2);
                        }
                    }
                    if (anySetter) {
                        indent("result.putAll(__metadata);", sb, 2);
                    }
                    indent("return result;", sb, 2);
                    indent("}", sb, 1);
                }
                if (params.generateToJSON()) {
                    sb.append("\n");
                    indent("public String toJSON() throws JsonProcessingException {", sb, 1);
                    indent("return new ObjectMapper().writeValueAsString(toMap());", sb, 2);
                    indent("}", sb, 1);
                }

                sb.append("}\n");
            } catch (Exception e) {
                sb.append(e);
            }

            return sb.toString();
        }

        private void indent(String s, StringBuilder sb, int count) {
            char[] space = new char[count * 4];
            Arrays.fill(space, ' ');
            sb.append(new String(space)).append(s).append('\n');
        }

        final class GeneratedParameter implements Comparable<GeneratedParameter> {

            private final Param param;

            public GeneratedParameter(Param param) {
                this.param = param;
            }

            boolean isPrimitive() {
                try {
                    if (param.defaultValue() != null || param.required()) {
                        return param.type().isNumber();
                    }
                } catch (AnnotationTypeMismatchException e) {
                    processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR,
                            "Default value " + " is not a string", el);
                }
                return false;
            }

            boolean isRequired() {
                if (!param.defaultValue().isEmpty()) {
                    return false;
                }
                return param.required();
            }

            String fieldName() {
                return '_' + param.value();
            }

            String loadClause() {
                try {
                    String nameQuoted = '"' + param.value() + '"';
                    StringBuilder sb = new StringBuilder();
                    String defVal = param.defaultValue().isEmpty() ? null : param.defaultValue();
                    if (defVal != null) {
                        defVal = defVal.trim();
                        switch (param.type()) {
                        case LONG:
                            if (!defVal.endsWith("L")) {
                                defVal += "L";
                            }
                            break;
                        case STRING:
                        case NON_EMPTY_STRING:
                            defVal = '"' + defVal.replaceAll("\"", "\\\"") + '"';
                            break;
                        }
                        Problems problems = new Problems();
                        param.type().validator().validate(problems, param.value(), defVal);
                        if (problems.hasFatal()) {
                            processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR,
                                    "Bad default value for " + param.value() + ":" + problems.getLeadProblem(), el);
                        }
                    }
                    if (param.required() && defVal == null) {
                        switch (param.type()) {
                        case STRING:
                        case NON_EMPTY_STRING:
                            sb.append("params.get(").append(nameQuoted).append(")");
                            break;
                        default:
                            sb.append(param.type().conversionMethod()).append("(").append("params.get(")
                                    .append(nameQuoted).append("))");
                        }
                        //                        sb.append("params.get(").append(nameQuoted).append(").").append(param.type().conversionMethod());
                    } else if (defVal != null) {
                        switch (param.type()) {
                        case STRING:
                        case NON_EMPTY_STRING:
                            sb.append("params.get(").append(nameQuoted)
                                    .append(") == null ? " + defVal + " : params.get(").append(nameQuoted)
                                    .append(")");
                            break;
                        default:
                            sb.append("params.get(").append(nameQuoted).append(") == null ? ").append(defVal)
                                    .append(" : ").append(param.type().conversionMethod()).append("(")
                                    .append("params.get(").append(nameQuoted).append("))");
                        }
                        //                        sb.append("params.get(").append(nameQuoted).append(").").append(param.type().conversionMethod());
                    } else {
                        switch (param.type()) {
                        case STRING:
                        case NON_EMPTY_STRING:
                            sb.append("Optional." + fromNullable + "(params.get(").append(nameQuoted)
                                    .append(") == null ? " + defVal + " : params.get(").append(nameQuoted)
                                    .append("))");
                            break;
                        default:
                            sb.append("Optional." + fromNullable + "(params.get(").append(nameQuoted)
                                    .append(") == null ? ").append(defVal).append(" : ")
                                    .append(param.type().conversionMethod()).append("(").append("params.get(")
                                    .append(nameQuoted).append(")))");
                        }
                    }
                    sb.append(";");
                    return sb.toString();
                } catch (Exception ex) {
                    return "null;\n if (true) throw new UnsupportedOperatiionException(\""
                            + ex.getMessage().replace('"', '\'') + "\")";
                }
            }

            String varDeclaration() {
                try {
                    return "private final "
                            + param.type().typeName(param.required() || !param.defaultValue().isEmpty(), true) + ' '
                            + fieldName() + ";";
                } catch (Exception e) {
                    return "// invalid annotation data: " + e.getMessage();
                }
            }

            public String returnType() {
                try {
                    if (!param.defaultValue().isEmpty()) {
                        return param.type().typeName(true);
                    }
                    return param.type().typeName(isRequired(), true);
                } catch (Exception e) {
                    return "Object /* invalid annotation data: " + e.getMessage() + "  */";
                }
            }

            @Override
            public int compareTo(GeneratedParameter t) {
                return param.value().compareTo(t.param.value());
            }

            @Override
            public String toString() {
                return "public " + returnType() + " get" + capitalize(param.value()) + "() {\n        return _"
                        + param.value() + ";\n    }\n";
            }

            @SuppressWarnings("null")
            private String capitalize(String s) {
                if (s.isEmpty() || s == null) {
                    return "";
                }
                char[] c = s.toCharArray();
                c[0] = Character.toUpperCase(c[0]);
                return new String(c);
            }
        }
    }
}