package org.fenixedu.bennu.core.groups; import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Set; import java.util.TreeMap; import org.fenixedu.bennu.core.annotation.GroupArgument; import org.fenixedu.bennu.core.annotation.GroupArgumentParser; import org.fenixedu.bennu.core.annotation.GroupOperator; import org.fenixedu.bennu.core.domain.exceptions.BennuCoreDomainException; import org.joda.time.DateTime; import com.google.common.base.Strings; public class CustomGroupRegistry { public static final String ARGUMENT_NAME_AS_FIELD_NAME = "QEHyHHkCEL6F2L0wZA6R"; private static class CustomGroupMetadata { private final String operator; private final Class<? extends CustomGroup> type; private final Constructor<? extends CustomGroup> constructor; private final Map<String, Field> fields = new HashMap<>(); public CustomGroupMetadata(Class<? extends CustomGroup> type) { GroupOperator operator = type.getAnnotation(GroupOperator.class); this.operator = operator.value(); if (!GroupParser.isValidIdentifier(this.operator)) { throw BennuCoreDomainException.invalidGroupIdentifier(this.operator); } this.type = type; this.constructor = findConstructor(type); Class<?> current = type; while (current != null) { for (Field field : current.getDeclaredFields()) { GroupArgument argument = field.getAnnotation(GroupArgument.class); if (argument != null) { String name = argument.value().equals(ARGUMENT_NAME_AS_FIELD_NAME) ? field.getName() : argument.value(); if (fields.containsKey(name)) { throw new Error("Duplicate field for name: '" + name + "'"); } if (!name.isEmpty() && !GroupParser.isValidIdentifier(name)) { throw BennuCoreDomainException.invalidGroupIdentifier(name); } fields.put(name, field); field.setAccessible(true); } } current = current.getSuperclass(); } } private Constructor<? extends CustomGroup> findConstructor(Class<? extends CustomGroup> type) { try { Constructor<? extends CustomGroup> constructor = type.getDeclaredConstructor(); constructor.setAccessible(true); return constructor; } catch (NoSuchMethodException e) { throw new Error("Custom Group type " + type.getName() + " does not declare a default constructor", e); } } public CustomGroup parse(Map<String, List<String>> arguments) { try { CustomGroup group = constructor.newInstance(); if (arguments != null) { for (Entry<String, List<String>> entry : arguments.entrySet()) { Field field = fields.get(entry.getKey()); if (field != null) { Collection<String> args = entry.getValue(); if (args != null && !args.isEmpty()) { field.set(group, parseField(field, args)); } } else { throw new Error("No field found with name '" + entry.getKey() + "' on group "); } } } return group; } catch (InstantiationException | IllegalAccessException | IllegalArgumentException | InvocationTargetException e) { throw new Error(e); } } private Object parseField(Field field, Collection<String> fieldArgs) throws InstantiationException, IllegalAccessException { if (Collection.class.isAssignableFrom(field.getType())) { ArgumentParser<Object> parser = parsers.get(determineCollectionType(field)); Collection<Object> value; if (Set.class.isAssignableFrom(field.getType())) { value = new HashSet<>(); } else if (List.class.isAssignableFrom(field.getType())) { value = new ArrayList<>(); } else { throw new Error("CustomGroups cannot handle field arguments of types other than Set, List or Array. Given: " + field.getType().getName()); } for (String arg : fieldArgs) { value.add(parser.parse(arg)); } return value; } else if (field.getType().isArray()) { ArgumentParser<Object> parser = parsers.get(field.getType().getComponentType()); Object[] value = new Object[fieldArgs.size()]; int i = 0; for (String arg : fieldArgs) { value[i++] = parser.parse(arg); } return value; } if (fieldArgs.size() == 1) { ArgumentParser<Object> parser = parsers.get(field.getType()); return parser.parse(fieldArgs.iterator().next()); } throw new Error("Invalid argument: " + field.getType().getName() + " " + field.getName()); } private Class<?> determineCollectionType(Field field) { ParameterizedType fieldElementType = (ParameterizedType) field.getGenericType(); Type[] args = fieldElementType.getActualTypeArguments(); if (args.length == 1) { return (Class<?>) args[0]; } throw new Error("Could not determine element type of collection typed argument: " + field); } public String getExpression(CustomGroup group) { try { List<String> arguments = new ArrayList<>(); for (String name : fields.keySet()) { List<String> values = serializeField(fields.get(name), group); if (!values.isEmpty()) { String serialized = String.join(", ", values); if (Strings.isNullOrEmpty(name)) { arguments.add(serialized); } else if (values.size() == 1) { arguments.add(name + "=" + serialized); } else { arguments.add(name + "=[" + serialized + "]"); } } } return arguments.isEmpty() ? operator : operator + "(" + String.join(", ", arguments) + ")"; } catch (IllegalArgumentException | IllegalAccessException e) { throw new Error(e); } } private List<String> serializeField(Field field, CustomGroup group) throws IllegalArgumentException, IllegalAccessException { List<String> serialized = new ArrayList<>(); if (Iterable.class.isAssignableFrom(field.getType())) { ArgumentParser<Object> parser = parsers.get(determineCollectionType(field)); Iterable<?> iterable = (Iterable<?>) field.get(group); if (iterable != null) { for (Object object : iterable) { serialized.add(parser.serialize(object)); } } } else if (field.getType().isArray()) { ArgumentParser<Object> parser = parsers.get(field.getType().getComponentType()); Object[] value = (Object[]) field.get(group); if (value != null) { for (Object object : value) { serialized.add(parser.serialize(object)); } } } else { Object value = field.get(group); if (value != null) { ArgumentParser<Object> parser = parsers.get(field.getType()); serialized.add(parser.serialize(value)); } } serialized.replaceAll(value -> GroupParser.isValidIdentifier(value) ? value : "'" + value.replace("'", "\\'") + "'"); return serialized; } } /* * Here I chose using a TreeMap instead of a HashMap, as the String instances that are * passed in the group parsing process are never reused, thus the string's hashCode must * be continuously recomputed. */ private static final TreeMap<String, CustomGroupMetadata> metadata = new TreeMap<>(); private static final Map<Class<?>, ArgumentParser<Object>> parsers = new HashMap<>(); private static final CustomGroupMetadata resolveMetadata(String operator) { CustomGroupMetadata group = metadata.get(operator); if (group == null) { throw BennuCoreDomainException.groupParsingNoGroupForOperator(operator); } return group; } public static CustomGroup parse(String operator, Map<String, List<String>> arguments) { return resolveMetadata(operator).parse(arguments); } public static String getExpression(CustomGroup group) { GroupOperator operator = group.getClass().getAnnotation(GroupOperator.class); return resolveMetadata(operator.value()).getExpression(group); } public static void registerCustomGroup(Class<? extends CustomGroup> type) { CustomGroupMetadata m = new CustomGroupMetadata(type); if (metadata.containsKey(m.operator)) { throw new Error("CustomGroup: duplicate operator name: " + m.operator); } metadata.put(m.operator, m); } public static void registerArgumentParser(Class<? extends ArgumentParser<?>> parserType) { try { @SuppressWarnings("unchecked") ArgumentParser<Object> parser = (ArgumentParser<Object>) parserType.newInstance(); if (parsers.containsKey(parser.type())) { throw new Error("GroupArgumentParser: duplicate parser for type: " + parser.type().getName()); } parsers.put(parser.type(), parser); } catch (InstantiationException | IllegalAccessException e) { throw new Error("GroupArgumentParser: could not instantiate parser of type: " + parserType.getName()); } } @GroupArgumentParser public static class ByteParser implements ArgumentParser<Byte> { @Override public Byte parse(String argument) { return Byte.valueOf(argument); } @Override public String serialize(Byte argument) { return argument.toString(); } @Override public Class<Byte> type() { return Byte.class; } } @GroupArgumentParser public static class ShortParser implements ArgumentParser<Short> { @Override public Short parse(String argument) { return Short.valueOf(argument); } @Override public String serialize(Short argument) { return argument.toString(); } @Override public Class<Short> type() { return Short.class; } } @GroupArgumentParser public static class IntegerParser implements ArgumentParser<Integer> { @Override public Integer parse(String argument) { return Integer.valueOf(argument); } @Override public String serialize(Integer argument) { return argument.toString(); } @Override public Class<Integer> type() { return Integer.class; } } @GroupArgumentParser public static class LongParser implements ArgumentParser<Long> { @Override public Long parse(String argument) { return Long.valueOf(argument); } @Override public String serialize(Long argument) { return argument.toString(); } @Override public Class<Long> type() { return Long.class; } } @GroupArgumentParser public static class FloatParser implements ArgumentParser<Float> { @Override public Float parse(String argument) { return Float.valueOf(argument); } @Override public String serialize(Float argument) { return argument.toString(); } @Override public Class<Float> type() { return Float.class; } } @GroupArgumentParser public static class DoubleParser implements ArgumentParser<Double> { @Override public Double parse(String argument) { return Double.valueOf(argument); } @Override public String serialize(Double argument) { return argument.toString(); } @Override public Class<Double> type() { return Double.class; } } @GroupArgumentParser public static class BooleanParser implements ArgumentParser<Boolean> { @Override public Boolean parse(String argument) { return Boolean.valueOf(argument); } @Override public String serialize(Boolean argument) { return argument.toString(); } @Override public Class<Boolean> type() { return Boolean.class; } } @GroupArgumentParser public static class CharacterParser implements ArgumentParser<Character> { @Override public Character parse(String argument) { if (argument.length() == 1) { return Character.valueOf(argument.charAt(0)); } throw new IllegalArgumentException(); } @Override public String serialize(Character argument) { return argument.toString(); } @Override public Class<Character> type() { return Character.class; } } @GroupArgumentParser public static class StringParser implements ArgumentParser<String> { @Override public String parse(String argument) { return argument; } @Override public String serialize(String argument) { return argument; } @Override public Class<String> type() { return String.class; } } @GroupArgumentParser public static class DateTimeParser implements ArgumentParser<DateTime> { @Override public DateTime parse(String argument) { return DateTime.parse(argument); } @Override public String serialize(DateTime argument) { return argument.toString(); } @Override public Class<DateTime> type() { return DateTime.class; } } }