@Override
  public ValidationResult validateRootElement(RootElementDeclaration rootElementDeclaration) {
    ValidationResult result = super.validateRootElement(rootElementDeclaration);
    String namespace = rootElementDeclaration.getNamespace();
    if (namespace == null) {
      namespace = "";
    }

    if (namespace.isEmpty()) {
      result.addError(rootElementDeclaration, "Root element should not be in the empty namespace.");
    }

    if (rootElementDeclaration.getName().toLowerCase().startsWith("web")) {
      result.addWarning(
          rootElementDeclaration,
          "You probably don't want a root element that starts with the name 'web'. Consider renaming using the @XmlRootElement annotation.");
    }

    JsonElementWrapper elementWrapper =
        rootElementDeclaration.getAnnotation(JsonElementWrapper.class);
    if (namespace.startsWith(CommonModels.GEDCOMX_DOMAIN) && elementWrapper == null) {
      result.addWarning(
          rootElementDeclaration,
          "Root elements in the '"
              + CommonModels.GEDCOMX_DOMAIN
              + "' namespace should probably be annotated with @"
              + JsonElementWrapper.class.getSimpleName()
              + ".");
    }

    if (elementWrapper != null) {
      String jsonName = elementWrapper.namespace() + elementWrapper.name();
      Declaration previous = this.jsonNameDeclarations.put(jsonName, rootElementDeclaration);
      if (previous != null) {
        result.addError(
            rootElementDeclaration,
            "JSON name conflict with " + String.valueOf(previous.getPosition()));
      }
    }

    return result;
  }
 @Override
 public ValidationResult validate(EnunciateFreemarkerModel model) {
   ValidationResult result = super.validate(model);
   for (SchemaInfo schemaInfo : model.getNamespacesToSchemas().values()) {
     for (Registry registry : schemaInfo.getRegistries()) {
       Collection<LocalElementDeclaration> localElements = registry.getLocalElementDeclarations();
       for (LocalElementDeclaration localElement : localElements) {
         JsonElementWrapper elementWrapper = localElement.getAnnotation(JsonElementWrapper.class);
         if (elementWrapper != null) {
           String jsonName = elementWrapper.namespace() + elementWrapper.name();
           Declaration previous = this.jsonNameDeclarations.put(jsonName, localElement);
           if (previous != null) {
             result.addError(
                 localElement,
                 "JSON name conflict with " + String.valueOf(previous.getPosition()));
           }
         }
       }
     }
   }
   return result;
 }
  public ValidationResult validateTypeDefinition(TypeDefinition typeDef) {
    ValidationResult result = new ValidationResult();

    // heatonra: using @XmlSeeAlso for a QName enum doesn't actually include the xmlns declaration,
    // so there's no reason to validate it here...
    // heatonra: I couldn't figure out a good way to validate the use of @XmlSeeAlso for extended
    // types at build-time. I think we'll have to rely on unit tests to validate this.

    if ("".equals(typeDef.getNamespace())) {
      result.addError(typeDef, "Type definition should not be in the empty namespace.");
    }

    if (typeDef.getName().toLowerCase().startsWith("web")) {
      result.addWarning(
          typeDef,
          "You probably don't want a type definition that starts with the name 'web'. Consider renaming using the @XmlType annotation.");
    }

    Collection<Attribute> attributes = typeDef.getAttributes();
    if (attributes != null && !attributes.isEmpty()) {
      for (Attribute attribute : attributes) {
        boolean isURI =
            ((DecoratedTypeMirror) TypeMirrorDecorator.decorate(attribute.getAccessorType()))
                .isInstanceOf("org.gedcomx.common.URI");
        if (isURI && !KnownXmlType.ANY_URI.getQname().equals(attribute.getBaseType().getQname())) {
          result.addError(
              attribute,
              "Accessors of type 'org.gedcomx.common.URI' should of type xs:anyURI. Please annotate the attribute with @XmlSchemaType(name = \"anyURI\", namespace = XMLConstants.W3C_XML_SCHEMA_NS_URI)");
        }

        if ("id".equalsIgnoreCase(attribute.getName())) {
          if (!attribute.isXmlID()) {
            result.addError(attribute, "Id attributes should be annotated as @XmlID.");
          }
        }

        TypeMirror accessorType = attribute.getAccessorType();
        if (accessorType instanceof EnumType
            && ((EnumType) accessorType).getDeclaration().getAnnotation(XmlQNameEnum.class)
                != null) {
          result.addError(
              attribute,
              "Accessors should not reference QName enums directly. You probably want to annotate this accessor with @XmlTransient.");
        }
      }
    }

    Value value = typeDef.getValue();
    if (value != null) {
      TypeMirror accessorType = value.getAccessorType();
      if (accessorType instanceof EnumType
          && ((EnumType) accessorType).getDeclaration().getAnnotation(XmlQNameEnum.class) != null) {
        result.addError(
            value,
            "Accessors should not reference QName enums directly. You probably want to annotate this accessor with @XmlTransient.");
      }

      boolean isURI =
          ((DecoratedTypeMirror) TypeMirrorDecorator.decorate(accessorType))
              .isInstanceOf("org.gedcomx.common.URI");
      if (isURI && !KnownXmlType.ANY_URI.getQname().equals(value.getBaseType().getQname())) {
        result.addError(
            value,
            "Accessors of type 'org.gedcomx.common.URI' should of type xs:anyURI. Please annotate the value with @XmlSchemaType(name = \"anyURI\", namespace = XMLConstants.W3C_XML_SCHEMA_NS_URI)");
      }
    }

    Collection<Element> elements = typeDef.getElements();
    if (elements != null && !elements.isEmpty()) {
      for (Element element : elements) {
        for (Element choice : element.getChoices()) {
          boolean isURI =
              ((DecoratedTypeMirror) TypeMirrorDecorator.decorate(choice.getAccessorType()))
                  .isInstanceOf("org.gedcomx.common.URI");
          if (isURI && !KnownXmlType.ANY_URI.getQname().equals(choice.getBaseType().getQname())) {
            result.addError(
                choice,
                "Accessors of type 'org.gedcomx.common.URI' should of type xs:anyURI. Please annotate the element with @XmlSchemaType(name = \"anyURI\", namespace = XMLConstants.W3C_XML_SCHEMA_NS_URI)");
          }

          if ("href".equals(choice.getName())) {
            result.addError(
                choice,
                "Entity links should be make with an attribute named 'href'. You probably need to apply @XmlAttribute.");
          }

          TypeMirror accessorType = choice.getAccessorType();
          if (accessorType instanceof EnumType
              && ((EnumType) accessorType).getDeclaration().getAnnotation(XmlQNameEnum.class)
                  != null) {
            result.addError(
                choice,
                "Accessors should not reference QName enums directly. You probably want to annotate this accessor with @XmlTransient.");
          }

          QName ref = choice.getRef();
          String ns = ref != null ? ref.getNamespaceURI() : choice.getNamespace();
          if (ns == null || "".equals(ns)) {
            result.addError(choice, "Choice should not reference the empty namespace.");
          }
        }

        if (element.isCollectionType()) {
          if (!element.isWrapped() && element.getName().endsWith("s")) {
            if (!suppressWarning(element, "gedcomx:plural_xml_name")) {
              result.addWarning(
                  element,
                  "You may want to use @XmlElement to change the name to a non-plural form.");
            }
          } else {
            // make sure collection types have a proper json name.
            String jsonMemberName = element.getJsonMemberName();
            if (!jsonMemberName.endsWith("s")) {
              if (!suppressWarning(element, "gedcomx:non_plural_json_name")) {
                result.addWarning(
                    element,
                    "Collection element should probably have a JSON name that ends with 's'. Consider annotating it with @JsonName.");
              }
            } else if (!element.isWrapped()) {
              if (element.getDelegate() instanceof PropertyDeclaration) {
                DecoratedMethodDeclaration getter =
                    ((PropertyDeclaration) element.getDelegate()).getGetter();
                if (getter == null
                    || getter.getAnnotation(JsonProperty.class) == null
                    || !jsonMemberName.equals(getter.getAnnotation(JsonProperty.class).value())) {
                  result.addWarning(
                      element,
                      "Collection element is annotated with @JsonName, but the getter needs to also be annotated with @JsonProperty(\""
                          + jsonMemberName
                          + "\").");
                }
                DecoratedMethodDeclaration setter =
                    ((PropertyDeclaration) element.getDelegate()).getSetter();
                if (setter == null
                    || setter.getAnnotation(JsonProperty.class) == null
                    || !jsonMemberName.equals(setter.getAnnotation(JsonProperty.class).value())) {
                  result.addWarning(
                      element,
                      "Collection element is annotated with @JsonName, but the setter needs to also be annotated with @JsonProperty(\""
                          + jsonMemberName
                          + "\").");
                }
              } else if (element.getDelegate() instanceof FieldDeclaration) {
                if (element.getAnnotation(JsonProperty.class) == null
                    || !jsonMemberName.equals(element.getAnnotation(JsonProperty.class).value())) {
                  result.addWarning(
                      element,
                      "Collection element is annotated with @JsonName, but the field needs to also be annotated with @JsonProperty(\""
                          + jsonMemberName
                          + "\").");
                }
              }
            }
          }
        }
      }
    }

    AnyElement anyElement = typeDef.getAnyElement();
    if (anyElement != null) {
      if (!isInstanceOf(typeDef, SupportsExtensionElements.class.getName())) {
        result.addError(
            anyElement,
            "Type definitions that supply the 'any' element must implement "
                + SupportsExtensionElements.class.getName()
                + " so the 'any' elements can be serialized to/from JSON.");
      }

      if (!"extensionElements".equals(anyElement.getSimpleName())) {
        if (!suppressWarning(anyElement, "gedcomx:unconventional_any_element_name")) {
          result.addWarning(
              anyElement,
              "The 'any' element might be better named 'extensionElements' to conform to convention.");
        }
      }

      JsonIgnore getterIgnore =
          anyElement.getDelegate() instanceof PropertyDeclaration
              ? ((PropertyDeclaration) anyElement.getDelegate())
                  .getGetter()
                  .getAnnotation(JsonIgnore.class)
              : anyElement.getAnnotation(JsonIgnore.class);
      JsonIgnore setterIgnore =
          anyElement.getDelegate() instanceof PropertyDeclaration
              ? ((PropertyDeclaration) anyElement.getDelegate())
                  .getGetter()
                  .getAnnotation(JsonIgnore.class)
              : getterIgnore;
      if (getterIgnore == null || setterIgnore == null) {
        String message =
            "Properties annotated with @XmlAnyElement should be annotated with @JsonIgnore.";
        if (anyElement.getDelegate() instanceof PropertyDeclaration) {
          message += " (On both the getter and the setter.)";
        }
        result.addError(anyElement, message);
      }
    }

    if (typeDef.isHasAnyAttribute()) {
      if (!isInstanceOf(typeDef, SupportsExtensionAttributes.class.getName())) {
        result.addError(
            anyElement,
            "Type definitions that supply the 'any' attribute must implement "
                + SupportsExtensionAttributes.class.getName()
                + " so the 'any' attributes can be serialized to/from JSON.");
      }

      MemberDeclaration anyAttribute = null;
      for (PropertyDeclaration prop : typeDef.getProperties()) {
        if (prop.getAnnotation(XmlAnyAttribute.class) != null) {
          anyAttribute = prop;
        }
      }

      if (anyAttribute == null) {
        // must be a field.
        for (FieldDeclaration field : typeDef.getFields()) {
          if (field.getAnnotation(XmlAnyAttribute.class) != null) {
            anyAttribute = field;
          }
        }
      }

      if (anyAttribute != null) {
        if (!"extensionAttributes".equals(anyAttribute.getSimpleName())) {
          if (!suppressWarning(anyElement, "gedcomx:unconventional_any_attribute_name")) {
            result.addWarning(
                anyElement,
                "The 'any' attribute might be better named 'extensionAttributes' to conform to convention.");
          }
        }

        JsonIgnore getterIgnore =
            anyAttribute instanceof PropertyDeclaration
                ? ((PropertyDeclaration) anyAttribute).getGetter().getAnnotation(JsonIgnore.class)
                : anyAttribute.getAnnotation(JsonIgnore.class);
        JsonIgnore setterIgnore =
            anyAttribute instanceof PropertyDeclaration
                ? ((PropertyDeclaration) anyAttribute).getGetter().getAnnotation(JsonIgnore.class)
                : getterIgnore;
        if (getterIgnore == null || setterIgnore == null) {
          String message =
              "Properties annotated with @XmlAnyAttribute should be annotated with @JsonIgnore.";
          if (anyAttribute instanceof PropertyDeclaration) {
            message += " (On both the getter and the setter.)";
          }
          result.addError(anyAttribute, message);
        }
      }
    }

    return result;
  }