package core.framework.impl.validate.type; import core.framework.api.util.Exceptions; import core.framework.api.util.Maps; import core.framework.api.util.Sets; import core.framework.impl.reflect.Fields; import javax.xml.bind.annotation.XmlAccessType; import javax.xml.bind.annotation.XmlAccessorType; import javax.xml.bind.annotation.XmlElement; import javax.xml.bind.annotation.XmlEnum; import javax.xml.bind.annotation.XmlEnumValue; import javax.xml.bind.annotation.XmlTransient; import java.lang.reflect.Field; import java.lang.reflect.Type; import java.math.BigDecimal; import java.time.Instant; import java.time.LocalDate; import java.time.LocalDateTime; import java.time.ZonedDateTime; import java.util.Map; import java.util.Set; /** * @author neo */ public class JAXBTypeValidator implements TypeVisitor { public static <T extends Enum<?>> void validateEnumClass(Class<T> enumClass) { if (enumClass.isAnnotationPresent(XmlEnum.class)) throw Exceptions.error("enum class must not have @XmlEnum, enumClass={}", enumClass.getCanonicalName()); T[] constants = enumClass.getEnumConstants(); for (T constant : constants) { try { Field enumField = enumClass.getField(constant.name()); if (!enumField.isAnnotationPresent(XmlEnumValue.class)) { throw Exceptions.error("enum must have @XmlEnumValue, enum={}", Fields.path(enumField)); } } catch (NoSuchFieldException e) { throw new Error(e); } } } protected final DataTypeValidator validator; private final Map<String, Set<String>> elements = Maps.newHashMap(); protected JAXBTypeValidator(Type instanceType) { validator = new DataTypeValidator(instanceType); validator.allowedValueClass = this::allowedValueClass; validator.allowChild = true; validator.allowTopLevelOptional = true; validator.visitor = this; } public void validate() { validator.validate(); } private boolean allowedValueClass(Class<?> valueClass) { return String.class.equals(valueClass) || Integer.class.equals(valueClass) || Boolean.class.equals(valueClass) || Long.class.equals(valueClass) || Double.class.equals(valueClass) || BigDecimal.class.equals(valueClass) || LocalDate.class.equals(valueClass) || LocalDateTime.class.equals(valueClass) || ZonedDateTime.class.equals(valueClass) || Instant.class.equals(valueClass) || valueClass.isEnum(); } @Override public void visitClass(Class<?> objectClass, String path) { XmlAccessorType accessorType = objectClass.getDeclaredAnnotation(XmlAccessorType.class); if (accessorType == null || accessorType.value() != XmlAccessType.FIELD) throw Exceptions.error("class must have @XmlAccessorType(XmlAccessType.FIELD), class={}", objectClass.getCanonicalName()); } @Override public void visitField(Field field, String parentPath) { XmlElement element = field.getDeclaredAnnotation(XmlElement.class); if (element == null) throw Exceptions.error("field must have @XmlElement(name=), field={}", Fields.path(field)); if (field.isAnnotationPresent(XmlTransient.class)) throw Exceptions.error("field must not have @XmlTransient, field={}", Fields.path(field)); String name = element.name(); if ("##default".equals(name)) { throw Exceptions.error("@XmlElement must have name attribute, field={}", Fields.path(field)); } Set<String> elements = this.elements.computeIfAbsent(parentPath, key -> Sets.newHashSet()); if (elements.contains(name)) { throw Exceptions.error("found duplicate element, field={}, name={}", Fields.path(field), name); } elements.add(name); Class<?> fieldClass = field.getType(); if (fieldClass.isEnum()) { @SuppressWarnings("unchecked") Class<? extends Enum<?>> enumClass = (Class<? extends Enum<?>>) fieldClass; validateEnumClass(enumClass); } } }