Generator.java

package net.morimekta.terminal.args.impl;

import net.morimekta.strings.NamingUtil;
import net.morimekta.terminal.args.ArgNameFormat;
import net.morimekta.terminal.args.ArgParser;
import net.morimekta.terminal.args.Flag;
import net.morimekta.terminal.args.Option;
import net.morimekta.terminal.args.Property;
import net.morimekta.terminal.args.ValueParser;
import net.morimekta.terminal.args.annotations.ArgIgnore;
import net.morimekta.terminal.args.annotations.ArgIsHidden;
import net.morimekta.terminal.args.annotations.ArgIsRepeated;
import net.morimekta.terminal.args.annotations.ArgIsRequired;
import net.morimekta.terminal.args.annotations.ArgKeyParser;
import net.morimekta.terminal.args.annotations.ArgNaming;
import net.morimekta.terminal.args.annotations.ArgOptions;
import net.morimekta.terminal.args.reference.AdderInstanceReference;
import net.morimekta.terminal.args.reference.AdderMethodReference;
import net.morimekta.terminal.args.reference.AdderReference;
import net.morimekta.terminal.args.reference.ChainedAdderReference;
import net.morimekta.terminal.args.reference.ChainedPutterReference;
import net.morimekta.terminal.args.reference.FieldReference;
import net.morimekta.terminal.args.reference.MethodReference;
import net.morimekta.terminal.args.reference.PutterInstanceReference;
import net.morimekta.terminal.args.reference.PutterMethodReference;
import net.morimekta.terminal.args.reference.PutterReference;
import net.morimekta.terminal.args.reference.Reference;
import net.morimekta.terminal.args.reference.SettableFieldReference;
import net.morimekta.terminal.args.reference.SettableMethodReference;
import net.morimekta.terminal.args.reference.SettableReference;

import java.lang.reflect.AccessibleObject;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.ParameterizedType;
import java.util.Collection;
import java.util.HashSet;
import java.util.Map;
import java.util.Optional;
import java.util.TreeMap;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import static java.lang.System.Logger.Level.TRACE;
import static net.morimekta.strings.NamingUtil.Format.CAMEL;
import static net.morimekta.strings.NamingUtil.Format.PASCAL;
import static net.morimekta.terminal.args.impl.GeneratorUtil.createCollectionSupplier;
import static net.morimekta.terminal.args.impl.GeneratorUtil.createMapSupplier;
import static net.morimekta.terminal.args.impl.GeneratorUtil.findField;
import static net.morimekta.terminal.args.impl.GeneratorUtil.findMethod;
import static net.morimekta.terminal.args.impl.GeneratorUtil.getAnnotation;
import static net.morimekta.terminal.args.impl.GeneratorUtil.getKeyParser;
import static net.morimekta.terminal.args.impl.GeneratorUtil.getShortChar;
import static net.morimekta.terminal.args.impl.GeneratorUtil.getShortName;
import static net.morimekta.terminal.args.impl.GeneratorUtil.getUsage;
import static net.morimekta.terminal.args.impl.GeneratorUtil.getValueParser;
import static net.morimekta.terminal.args.impl.GeneratorUtil.isDeclared;
import static net.morimekta.terminal.args.impl.GeneratorUtil.isPublic;
import static net.morimekta.terminal.args.impl.GeneratorUtil.isStatic;

public final class Generator {
    private static final System.Logger LOGGER = System.getLogger(Generator.class.getName());

    private Generator() {}

    public static ArgParser.Builder generateArgs(ArgParser.Builder args,
                                                 Object config,
                                                 String namePrefix,
                                                 ArgNameFormat defaultNameFormat) {
        generateArgsInternal(args, config, namePrefix, defaultNameFormat);
        return args;
    }

    private static boolean generateArgsInternal(
            ArgParser.Builder args,
            Object config,
            String namePrefix,
            ArgNameFormat parentNameFormat) {
        // when checking fields, if we need any of these methods to access, then ignore,
        // as the field should already have been handled.
        var handledMethods = new HashSet<Method>();
        var handledFields = new HashSet<Field>();
        var acceptedNames = new TreeMap<String, Reference>();
        var argumentNamingAnnotation = getAnnotation(config.getClass(), ArgNaming.class);
        var nameFormat = argumentNamingAnnotation != null
                         ? argumentNamingAnnotation.value()
                         : parentNameFormat;

        for (Method method : Stream.of(config.getClass().getMethods())
                                   .filter(m -> isPublic(m) && !isStatic(m))
                                   .sorted(Generator::compareMethods)
                                   .collect(Collectors.toList())) {
            var declaredPackage = method.getDeclaringClass().getPackage().getName();
            if (declaredPackage.startsWith("java.") ||
                declaredPackage.startsWith("javax.") ||
                declaredPackage.startsWith("kotlin.") ||
                handledMethods.contains(method)) {
                continue;
            }
            handledMethods.add(method);

            if (method.isAnnotationPresent(ArgIgnore.class)) {
                LOGGER.log(TRACE, "Ignored method " + method.getName() + "().");
                continue;
            }

            Matcher setterMatcher = SETTER.matcher(method.getName());
            Matcher getterMatcher = GETTER.matcher(method.getName());
            var isSetter = setterMatcher.matches();
            var isGetter = getterMatcher.matches();
            var methodNamePart = (!isSetter && !isGetter)
                                 ? method.getName()
                                 : (isSetter ? setterMatcher : getterMatcher).group("name");

            // Stupid thing kotlin does with annotations, especially if this is
            // a getter or setter for a field (automatic get/set in kotlin)...
            var field = findField(method.getDeclaringClass(), NamingUtil.format(methodNamePart, CAMEL));
            field.ifPresent(handledFields::add);

            // - The static getName$annotations -- kotlin thing.
            // - The associated field.
            // - The method itself.
            AccessibleObject annotationRef = findMethod(method.getDeclaringClass(),
                                                        "get" + methodNamePart + "$annotations")
                    .map(m -> (AccessibleObject) m)
                    .or(() -> Optional.ofNullable((AccessibleObject) field.orElse(null)))
                    .orElse(method);
            if (annotationRef.isAnnotationPresent(ArgIgnore.class)) {
                // Ignored based on associated annotation holder.
                LOGGER.log(TRACE, "Ignored method " + method.getName() + "(associated annotation).");
                continue;
            }

            var declared = isDeclared(method) || isDeclared(annotationRef);
            var argumentName = "";
            if (method.isAnnotationPresent(ArgOptions.class)) {
                argumentName = method.getAnnotation(ArgOptions.class).name();
            }
            if (argumentName.isEmpty() && annotationRef.isAnnotationPresent(ArgOptions.class)) {
                argumentName = annotationRef.getAnnotation(ArgOptions.class).name();
            }
            if (argumentName.isEmpty()) {
                argumentName = nameFormat.format(methodNamePart);
            }
            var name = "--" + parentNameFormat.join(namePrefix, argumentName);

            var isAdder = false;
            var isPutter = false;
            if (isSetter) {
                switch (method.getParameterCount()) {
                    case 0:
                        if (declared) {
                            throw new IllegalArgumentException("No parameters on setter: " + method.getName() + "().");
                        }
                        LOGGER.log(TRACE, "No parameters on setter: " + method.getName() + "().");
                        continue;
                    case 1:
                        if (method.getName().startsWith("add")) {
                            isAdder = true;
                        }
                        break;
                    case 2:
                        isPutter = true;
                        break;
                    default:
                        if (declared) {
                            throw new IllegalArgumentException("Too many parameters on setter: " + method.getName() + "().");
                        }
                        LOGGER.log(TRACE, "Too many parameters on setter: " + method.getName() + "().");
                        continue;
                }
            } else if (isGetter) {
                if (method.getParameterCount() > 0) {
                    if (declared) {
                        throw new IllegalArgumentException("Parameters on getter: " + method.getName() + "().");
                    }
                    LOGGER.log(TRACE, "Parameters on getter: " + method.getName() + "().");
                    continue;
                }
            } else {
                if (!declared) {
                    LOGGER.log(TRACE, "Ignoring method " + method.getName() + "(), unrecognized name pattern.");
                    continue;
                }
                switch (method.getParameterCount()) {
                    case 0:
                        isGetter = true;
                        break;
                    case 1:
                        isSetter = true;
                        break;
                    case 2:
                        isPutter = true;
                        isSetter = true;
                        break;
                    default:
                        throw new IllegalArgumentException(
                                "Unknown param count for arguments: " + method.getName() + " = " + method.getParameterCount());
                }
            }

            Optional<Method> setter = isSetter ? Optional.of(method) : Optional.empty();
            Optional<Method> getter = isGetter ? Optional.of(method) : Optional.empty();
            if (getter.isEmpty()) {
                getter = findMethod(method.getDeclaringClass(), "get" + methodNamePart)
                        .filter(GeneratorUtil::isPublic)
                        .or(() -> findMethod(method.getDeclaringClass(), "is" + methodNamePart))
                        .filter(GeneratorUtil::isPublic);
                getter.ifPresent(handledMethods::add);
            }

            if (acceptedNames.containsKey(name)) {
                // conflicting name
                throw new IllegalArgumentException("Conflicting name: " + method.getName() + " for " + name);
            }

            if (isPutter) {
                LOGGER.log(TRACE, "Putter method: " + method.getName() + "(): " + name);
                var reference = new PutterMethodReference(
                        config, getter.orElse(null), setter.orElse(null), annotationRef);
                if (addPutter(name, reference, args)) {
                    acceptedNames.put(name, reference);
                }
            } else if (isAdder) {
                LOGGER.log(TRACE, "Adder method: " + method.getName() + "(): " + name);
                var reference = new AdderMethodReference(
                        config, getter.orElse(null), setter.orElse(null), annotationRef);
                if (addAdder(name, reference, args)) {
                    acceptedNames.put(name, reference);
                }
            } else if (setter.isEmpty()) {
                LOGGER.log(TRACE, "Getter method: " + method.getName() + "(): " + name);
                var reference = new MethodReference(config, method, annotationRef);
                if (addOnImmutable(name, reference, args, nameFormat)) {
                    acceptedNames.put(name, reference);
                }
            } else {
                LOGGER.log(TRACE, "Setter method: " + method.getName() + "(): " + name);
                var reference = new SettableMethodReference(config, getter.orElse(null), method, annotationRef);
                if (addSettable(name, reference, args, nameFormat)) {
                    acceptedNames.put(name, reference);
                }
            }
        }

        var fieldArguments = new HashSet<String>();
        for (Field field : config.getClass().getFields()) {
            if ((field.getModifiers() & Modifier.STATIC) == Modifier.STATIC ||
                (field.getModifiers() & Modifier.PUBLIC) != Modifier.PUBLIC ||
                handledFields.contains(field)) {
                // We are not interested in static or non-public fields.
                continue;
            }
            if (field.isAnnotationPresent(ArgIgnore.class)) {
                LOGGER.log(TRACE, "Ignored field " + field.getName());
                continue;
            }

            var declared = isDeclared(field);
            var pascalName = NamingUtil.format(field.getName(), PASCAL);
            Optional<Method> getter =
                    findMethod(field.getDeclaringClass(), "get" + pascalName)
                            .or(() -> findMethod(field.getDeclaringClass(), "is" + pascalName))
                            .filter(GeneratorUtil::isPublic);
            getter.ifPresent(handledMethods::add);

            Optional<Method> setter = findMethod(field.getDeclaringClass(), "set" + pascalName, field.getType())
                    .filter(GeneratorUtil::isPublic);
            setter.ifPresent(handledMethods::add);

            var argumentName = "";
            if (field.isAnnotationPresent(ArgOptions.class)) {
                argumentName = field.getAnnotation(ArgOptions.class).name();
            }
            if (argumentName.isEmpty()) {
                argumentName = nameFormat.format(field.getName());
            }
            if (argumentName.isEmpty()) {
                if (declared) {
                    throw new IllegalArgumentException("Ignored field " + field.getName() + ", unable to generate name.");
                }
                LOGGER.log(TRACE, "Ignored field " + field.getName() + ", unable to generate name.");
                continue;
            }
            var name = "--" + parentNameFormat.join(namePrefix, argumentName);
            if (acceptedNames.containsKey(name)) {
                if (declared || fieldArguments.contains(name)) {
                    throw new IllegalArgumentException("Name conflict for field " + field.getName() + ": '" + name + "'.");
                }
                LOGGER.log(TRACE, "Ignored field " + field.getName() + ", already handled by method.");
                continue;
            }

            if ((field.getModifiers() & Modifier.FINAL) == Modifier.FINAL) {
                LOGGER.log(TRACE, "Immutable field " + field.getName() + " -> '" + name + "'.");
                var reference = new FieldReference(config, field, field);
                if (addOnImmutable(name, reference, args, nameFormat)) {
                    acceptedNames.put(name, reference);
                    fieldArguments.add(name);
                }
            } else {
                LOGGER.log(TRACE, "Settable field " + field.getName() + " -> '" + name + "'.");
                var reference = new SettableFieldReference(config, field, field);
                if (addSettable(name, reference, args, nameFormat)) {
                    acceptedNames.put(name, reference);
                    fieldArguments.add(name);
                }
            }
        }
        return acceptedNames.size() > 0;
    }

    public static boolean addSettable(
            String name,
            SettableReference ref,
            ArgParser.Builder args,
            ArgNameFormat defaultNameFormat) {
        var type = ref.getType();
        var isMap = Map.class.isAssignableFrom(type);
        // If this is a map type settable, it must be parsed as a property.
        // otherwise we may use a configured value parser as the parser for
        // the whole map, and not just for the map item value.
        // TODO: Allow a value parser, and check if the type of the parser
        //       is of map type, and use that to determine if it can be used
        //       as option or map property.
        var valueParser = isMap ? null : getValueParser(type, ref);
        if (valueParser != null) {
            return addOption(name, ref, valueParser, args);
        } else if (Boolean.class.isAssignableFrom(type) ||
                   Boolean.TYPE.isAssignableFrom(type)) {
            return addFlag(name, ref, args);
        } else if (isMap) {
            // putting values in map.
            var generic = ref.getGenericType();
            Class<?> keyType;
            Class<?> valueType;
            if (generic instanceof ParameterizedType) {
                var pt = (ParameterizedType) generic;
                if (pt.getActualTypeArguments()[0] instanceof Class &&
                    pt.getActualTypeArguments()[1] instanceof Class) {
                    keyType = (Class<?>) pt.getActualTypeArguments()[0];
                    valueType = (Class<?>) pt.getActualTypeArguments()[1];
                } else {
                    LOGGER.log(TRACE, " - Missing map generics for " + name + ".");
                    return false;
                }
            } else {
                if (isDeclared(ref)) {
                    throw new IllegalArgumentException("Missing map generics for " + name + ".");
                }
                LOGGER.log(TRACE, " - Missing map generics for " + name + ".");
                return false;
            }
            var mkMap = createMapSupplier(ref.getType());
            return addPutter(name, new ChainedPutterReference(ref, keyType, valueType, mkMap), args);
        } else if (Collection.class.isAssignableFrom(type)) {
            // adding values to collection.
            var generic = ref.getGenericType();
            Class<?> itemType;
            if (generic instanceof ParameterizedType) {
                var pt = (ParameterizedType) generic;
                if (pt.getActualTypeArguments()[0] instanceof Class) {
                    itemType = (Class<?>) pt.getActualTypeArguments()[0];
                } else {
                    LOGGER.log(TRACE, " - Missing collection generics for " + name + ".");
                    return false;
                }
            } else {
                if (isDeclared(ref)) {
                    throw new IllegalArgumentException("Missing collection generics for " + name + ".");
                }
                LOGGER.log(TRACE, " - Missing collection generics for " + name + ".");
                return false;
            }
            var mkCollection = createCollectionSupplier(ref.getType());
            return addAdder(name, new ChainedAdderReference(ref, itemType, mkCollection), args);
        } else {
            // possibly chained object arguments. This must already be instantiated.
            Object object = ref.get();
            if (object == null) {
                LOGGER.log(TRACE, " - Ignoring chained arguments for " + name + ", no object.");
                return false;
            }
            return addChained(name, object, args, defaultNameFormat);
        }
    }

    public static boolean addOnImmutable(
            String name,
            Reference ref,
            ArgParser.Builder args,
            ArgNameFormat defaultNameFormat) {
        // final, only for certain cases, in all instances the object must already exist:
        //  - for chained object arguments.
        //  - for putting values in map.
        //  - for adding values to collection.
        Object object = ref.get();
        if (object == null) {
            if (isDeclared(ref)) {
                throw new IllegalArgumentException("Null instance on declared argument on " + ref.getName() + ".");
            }
            LOGGER.log(TRACE, " - Ignoring chained arguments for " + name + " (immutable), no object.");
            return false;
        }

        // possibly chained object arguments.
        if (object instanceof Map) {
            var generic = ref.getGenericType();
            if (!(generic instanceof ParameterizedType)) {
                if (isDeclared(ref)) {
                    throw new IllegalArgumentException("Unable to determine map type of " + ref.getName() + ".");
                }
                LOGGER.log(TRACE, " - Unable to determine map type of " + ref.getName() + ".");
                return false;
            }
            var parameterizedType = (ParameterizedType) generic;
            if (parameterizedType.getActualTypeArguments().length != 2 ||
                !(parameterizedType.getActualTypeArguments()[0] instanceof Class) ||
                !(parameterizedType.getActualTypeArguments()[1] instanceof Class)) {
                if (isDeclared(ref)) {
                    throw new IllegalArgumentException("Unable to determine map types of " + ref.getName() + ".");
                }
                LOGGER.log(TRACE, " - Unable to determine map types of " + ref.getName() + ".");
                return false;
            }
            var keyType = (Class<?>) parameterizedType.getActualTypeArguments()[0];
            var valueType = (Class<?>) parameterizedType.getActualTypeArguments()[1];
            @SuppressWarnings("unchecked")
            var putter = new PutterInstanceReference(
                    ref.getName(),
                    keyType,
                    valueType,
                    (Map<Object, Object>) object,
                    ref);
            return addPutter(name, putter, args);
        } else if (object instanceof Collection) {
            var generic = ref.getGenericType();
            if (!(generic instanceof ParameterizedType)) {
                if (isDeclared(ref)) {
                    throw new IllegalArgumentException("Unable to determine collection type of " + ref.getName() + ".");
                }
                LOGGER.log(TRACE, " - Unable to determine collection type of " + ref.getName() + ".");
                return false;
            }
            var parameterizedType = (ParameterizedType) generic;
            if (parameterizedType.getActualTypeArguments().length != 1 ||
                !(parameterizedType.getActualTypeArguments()[0] instanceof Class)) {
                if (isDeclared(ref)) {
                    throw new IllegalArgumentException("Unable to determine collection type of " + ref.getName() + ".");
                }
                LOGGER.log(TRACE, " - Unable to determine collection type of " + ref.getName() + ".");
                return false;
            }
            var itemType = (Class<?>) parameterizedType.getActualTypeArguments()[0];
            @SuppressWarnings("unchecked")
            var adder = new AdderInstanceReference(
                    ref.getName(),
                    itemType,
                    (Collection<Object>) object,
                    ref);
            return addAdder(name, adder, args);
        } else {
            return addChained(name, object, args, defaultNameFormat);
        }
    }

    public static boolean addChained(
            String name,
            Object instance,
            ArgParser.Builder args,
            ArgNameFormat defaultNameFormat) {
        String subPrefix = name.substring(2);  // skip the "--" part.
        return generateArgsInternal(args, instance, subPrefix, defaultNameFormat);
    }

    public static boolean addAdder(String name, AdderReference ref, ArgParser.Builder args) {
        var itemParser = getValueParser(ref.getItemType(), ref);
        if (itemParser == null) {
            if (isDeclared(ref)) {
                throw new IllegalArgumentException(
                        "Unknown value parser for item type " + ref.getItemType().getSimpleName() +
                        " for " + name + ".");
            }
            LOGGER.log(TRACE, "Ignoring adder " + name + ", unknown item type.");
            return false;
        }

        var shortArgs = getShortName(ref);
        var usage = getUsage(ref);
        var builder = shortArgs.isEmpty()
                      ? Option.optionLong(name, usage, itemParser.andApply(ref::add))
                      : Option.option(name, shortArgs, usage, itemParser.andApply(ref::add));
        builder = builder.repeated();
        if (ref.isAnnotationPresent(ArgIsHidden.class)) {
            builder = builder.hidden();
        }
        if (ref.isAnnotationPresent(ArgIsRequired.class)) {
            builder = builder.required();
        }
        args.add(builder);
        return true;
    }

    public static boolean addPutter(String name, PutterReference ref, ArgParser.Builder args) {
        var keyParser = getKeyParser(ref.getKeyType(), ref);
        var valueParser = getValueParser(ref.getValueType(), ref);
        if (keyParser == null || valueParser == null) {
            if (isDeclared(ref)) {
                throw new IllegalArgumentException(
                        "Unknown value parser for key " + ref.getKeyType().getSimpleName() +
                        " or value " + ref.getValueType().getSimpleName() +
                        " for " + name + ".");
            }
            LOGGER.log(TRACE, "Ignoring putter " + name + ", unknown item type.");
            return false;
        }
        var putter = new Property.Putter() {
            @Override
            public void put(String key, String value) {
                var parsedKey = keyParser.parse(key);
                var parsedValue = valueParser.parse(value);
                ref.put(parsedKey, parsedValue);
            }
        };
        var shortArgs = getShortChar(ref);
        var usage = getUsage(ref);
        var builder = shortArgs == '\0' ?
                      Property.propertyLong(name, usage, putter) :
                      Property.property(name, shortArgs, usage, putter);
        if (ref.isAnnotationPresent(ArgIsHidden.class)) {
            builder = builder.hidden();
        }
        if (ref.isAnnotationPresent(ArgIsRequired.class)) {
            throw new IllegalArgumentException("Properties cannot be required: " + ref.getName());
        }
        args.add(builder);
        return true;
    }

    public static boolean addFlag(String name, SettableReference ref, ArgParser.Builder args) {
        var shortArgs = getShortName(ref);
        var usage = getUsage(ref);
        var builder = shortArgs.isEmpty() ?
                      Flag.flagLong(name, usage, ref::set) :
                      Flag.flag(name, shortArgs, usage, ref::set);
        if (ref.isAnnotationPresent(ArgIsHidden.class)) {
            builder = builder.hidden();
        }
        if (ref.isAnnotationPresent(ArgIsRepeated.class)) {
            builder = builder.repeated();
        }
        if (ref.isAnnotationPresent(ArgIsRequired.class)) {
            throw new IllegalArgumentException("Flag cannot be required: " + ref.getName());
        }
        if (ref.isAnnotationPresent(ArgKeyParser.class)) {
            throw new IllegalArgumentException("Invalid key parser override on flag: " + ref.getName());
        }
        args.add(builder);
        return true;
    }

    public static boolean addOption(String name,
                                    SettableReference ref,
                                    ValueParser<Object> parser,
                                    ArgParser.Builder args) {
        var shortArgs = getShortName(ref);
        var usage = getUsage(ref);
        var builder = shortArgs.isEmpty() ?
                      Option.optionLong(name, usage, parser.andApply(ref::set)) :
                      Option.option(name, shortArgs, usage, parser.andApply(ref::set));
        if (ref.isAnnotationPresent(ArgIsRepeated.class)) {
            builder = builder.repeated();
        }
        if (ref.isAnnotationPresent(ArgIsRequired.class)) {
            builder = builder.required();
        }
        if (ref.isAnnotationPresent(ArgIsHidden.class)) {
            builder = builder.hidden();
        }
        if (ref.isAnnotationPresent(ArgKeyParser.class)) {
            throw new IllegalArgumentException("Invalid key parser override on option: " + ref.getName());
        }
        args.add(builder);
        return true;
    }

    private static boolean isGetter(Method method) {
        return GETTER.matcher(method.getName()).matches();
    }

    private static boolean isSetter(Method method) {
        return SETTER.matcher(method.getName()).matches();
    }

    private static int compareMethods(Method a, Method b) {
        // Methods should be checked in the order:
        // - setters
        // - others
        // - getters
        // this way we should avoid setter -> getter collisions etc.
        var aSetter = isSetter(a);
        var bSetter = isSetter(b);
        var aGetter = isGetter(a);
        var bGetter = isGetter(b);
        if (aSetter == bSetter &&
            aGetter == bGetter) {
            // compare name within the group to be consistent.
            return a.getName().compareTo(b.getName());
        } else if (aSetter) {
            return -1;
        } else if (bSetter) {
            return 1;
        } else if (aGetter) {
            return 1;
        } else {
            // if bGetter, only option left.
            return -1;
        }
    }

    private static final Pattern SETTER = Pattern.compile(
            "^(?:set|addTo|add|putIn|put|with)(?<name>[A-Z0-9][a-zA-Z0-9]*)$");

    private static final Pattern GETTER = Pattern.compile(
            "^(?:get|is)(?<name>[A-Z0-9][a-zA-Z0-9]*)$");
}