private Notifications readNotifications(Element parent, Element fallbackParent) {
        Element notificationsElement = XML.getChild(parent, notificationsTag);
        if (notificationsElement == null)
            notificationsElement = XML.getChild(fallbackParent, notificationsTag);
        if (notificationsElement == null)
            return Notifications.none();

        When defaultWhen = stringAttribute("when", notificationsElement).map(When::fromValue).orElse(When.failingCommit);
        Map<When, List<String>> emailAddresses = new HashMap<>();
        Map<When, List<Role>> emailRoles = new HashMap<>();
        for (When when : When.values()) {
            emailAddresses.put(when, new ArrayList<>());
            emailRoles.put(when, new ArrayList<>());
        }

        for (Element emailElement : XML.getChildren(notificationsElement, "email")) {
            Optional<String> addressAttribute = stringAttribute("address", emailElement);
            Optional<Role> roleAttribute = stringAttribute("role", emailElement).map(Role::fromValue);
            When when = stringAttribute("when", emailElement).map(When::fromValue).orElse(defaultWhen);
            if (addressAttribute.isPresent() == roleAttribute.isPresent())
                illegal("Exactly one of 'role' and 'address' must be present in 'email' elements.");

            addressAttribute.ifPresent(address -> emailAddresses.get(when).add(address));
            roleAttribute.ifPresent(role -> emailRoles.get(when).add(role));
        }
        return Notifications.of(emailAddresses, emailRoles);
    }

    private void readEndpoints(Element parent, Optional<String> instance, List<Step> steps, List<Endpoint> endpoints,
                               Map<ClusterSpec.Id, Map<ZoneId, ZoneEndpoint>> zoneEndpoints) {
        var endpointsElement = XML.getChild(parent, endpointsTag);
        if (endpointsElement == null) return;

        Endpoint.Level level = instance.isEmpty() ? Endpoint.Level.application : Endpoint.Level.instance;
        Map<String, Map<RegionName, List<ZoneEndpoint>>> endpointsByZone = new LinkedHashMap<>();
        for (Element endpointElement : XML.getChildren(endpointsElement, endpointTag).stream() // Read zone settings first.
                .sorted(comparingInt(endpoint -> getZoneEndpointType(endpoint, level).isPresent() ? 0 : 1)).toList()) {
            Optional<Endpoint> endpoint = readEndpoint(parent, endpointElement, level, instance, steps, List.of(), endpointsByZone);
            endpoint.ifPresent(e -> endpoints.add(e));
        }
        validateAndConsolidate(endpointsByZone, zoneEndpoints);
    }

    /**
     * @param parentElement
     * @param endpointElement
     * @param level decide what this method is reading TODO: Split into different methods instead
     * @param instance the instance this applies to, or empty if it does not apply to an instance (application endpoints)
     * @param steps
     * @param forRegions the regions this applies to (for bcp), or empty (otherwise) to read this from "region" subelements
     * @param endpointsByZone a map containing any zone endpoints read by this
     * @return the endpoint read, unless it is added to endspointsByZone instead *sob*
     */
    static Optional<Endpoint> readEndpoint(Element parentElement,
                                           Element endpointElement,
                                           Endpoint.Level level,
                                           Optional<String> instance,
                                           List<Step> steps,
                                           Collection<RegionName> forRegions,
                                           Map<String, Map<RegionName, List<ZoneEndpoint>>> endpointsByZone) {
        String containerId = requireStringAttribute("container-id", endpointElement);
        Optional<String> endpointId = stringAttribute("id", endpointElement);
        Optional<String> zoneEndpointType = getZoneEndpointType(endpointElement, level);
        String msgPrefix = (level == Endpoint.Level.application ? "Application-level" : "Instance-level") + " endpoint" +
                           (zoneEndpointType.isEmpty() ? " '" + endpointId.orElse(Endpoint.DEFAULT_ID) + "': " : ": ");

        if (zoneEndpointType.isPresent() && endpointId.isPresent())
            illegal(msgPrefix + "cannot declare 'id' with type 'zone' or 'private'");

        String invalidChild = level == Endpoint.Level.application ? "region" : "instance";
        if ( ! XML.getChildren(endpointElement, invalidChild).isEmpty())
            illegal(msgPrefix + "invalid element '" + invalidChild + "'");

        boolean enabled = readEnabled(endpointElement, zoneEndpointType, msgPrefix);

        List<AllowedUrn> allowedUrns = readAllowedUrns(endpointElement, zoneEndpointType, containerId, msgPrefix);
        Optional<String> authMethod = stringAttribute("auth-method", endpointElement);

        if(authMethod.isPresent() && ! zoneEndpointType.orElse("").equals("private"))
            illegal(msgPrefix + "cannot declare 'auth-method' with endpoint type '" + zoneEndpointType.orElse("") + "', must be type 'private'");

        if(authMethod.isPresent() && !authMethod.get().equals("token") && !authMethod.get().equals("mtls"))
            illegal(msgPrefix + "cannot declare auth-method '" + authMethod.get() + "', must be one of 'mtls' and 'token'");

        List<Endpoint.Target> targets = new ArrayList<>();
        if (level == Endpoint.Level.application) {
            if ( ! forRegions.isEmpty()) throw new IllegalStateException("Illegal combination");
            Optional<String> endpointRegion = stringAttribute("region", endpointElement);
            int weightSum = 0;
            for (var instanceElement : XML.getChildren(endpointElement, "instance")) {
                String instanceName = instanceElement.getTextContent();
                if (instanceName == null || instanceName.isBlank()) illegal(msgPrefix + "empty 'instance' element");
                Optional<String> instanceRegion = stringAttribute("region", instanceElement);
                if (endpointRegion.isPresent() == instanceRegion.isPresent())
                    illegal(msgPrefix + "'region' attribute must be declared on either <endpoint> or <instance> tag");
                String weightFromAttribute = requireStringAttribute("weight", instanceElement);
                int weight;
                try {
                    weight = Integer.parseInt(weightFromAttribute);
                } catch (NumberFormatException e) {
                    throw new IllegalArgumentException(msgPrefix + "invalid weight value '" + weightFromAttribute + "'");
                }
                weightSum += weight;
                targets.add(new Endpoint.Target(RegionName.from(endpointRegion.orElseGet(instanceRegion::get)),
                                                InstanceName.from(instanceName),
                                                weight));
            }
            if (weightSum == 0) illegal(msgPrefix + "sum of all weights must be positive, got " + weightSum);
        } else {
            if (stringAttribute("region", endpointElement).isPresent()) illegal(msgPrefix + "invalid 'region' attribute");

            Set<RegionName> regions = new LinkedHashSet<>(forRegions);
            List<Element> regionElements = XML.getChildren(endpointElement, "region");
            if ( ! regions.isEmpty() && ! regionElements.isEmpty())
                illegal("Endpoints in <" + parentElement.getTagName() + "> cannot contain <region> children");
            for (var regionElement : XML.getChildren(endpointElement, "region")) {
                String region = regionElement.getTextContent();
                if (region == null || region.isBlank())
                    illegal(msgPrefix + "empty 'region' element");
                if (   zoneEndpointType.isEmpty()
                    && Stream.of(RegionName.from(region), null)
                             .map(endpointsByZone.getOrDefault(containerId, new HashMap<>())::get)
                             .flatMap(maybeEndpoints -> maybeEndpoints == null ? Stream.empty() : maybeEndpoints.stream())
                             .anyMatch(endpoint -> ! endpoint.isPublicEndpoint()))
                    illegal(msgPrefix + "targets zone endpoint in '" + region + "' with 'enabled' set to 'false'");
                if ( ! regions.add(RegionName.from(region)))
                    illegal(msgPrefix + "duplicate 'region' element: '" + region + "'");
            }

            if (zoneEndpointType.isPresent()) {
                if (regions.isEmpty()) regions.add(null);
                ZoneEndpoint endpoint = switch (zoneEndpointType.get()) {
                    case "zone" -> new ZoneEndpoint(enabled, false, List.of());
                    case "private" -> new ZoneEndpoint(true, true,  allowedUrns)  // Doesn't turn off public visibility.
                            .withAuthMethods(authMethod.orElse("").equals("token") ? List.of(AuthMethod.token) : List.of(AuthMethod.mtls));
                    default -> throw new IllegalArgumentException("unsupported zone endpoint type '" + zoneEndpointType.get() + "'");
                };
                for (RegionName region : regions) endpointsByZone.computeIfAbsent(containerId, __ -> new LinkedHashMap<>())
                                                                 .computeIfAbsent(region, __ -> new ArrayList<>())
                                                                 .add(endpoint);
            }
            else {
                if (regions.isEmpty()) {
                    // No explicit targets given for instance level endpoint. Include all declared, enabled regions by default
                    List<RegionName> declared =
                            steps.stream()
                                 .filter(step -> step.concerns(Environment.prod))
                                 .flatMap(step -> step.zones().stream())
                                 .flatMap(zone -> zone.region().stream())
                                 .toList();
                    if (declared.isEmpty()) illegal(msgPrefix + "no declared regions to target");

                    declared.stream().filter(region -> Stream.of(region, null)
                                                             .map(endpointsByZone.getOrDefault(containerId, new HashMap<>())::get)
                                                             .flatMap(maybeEndpoints -> maybeEndpoints == null ? Stream.empty() : maybeEndpoints.stream())
                                                             .allMatch(ZoneEndpoint::isPublicEndpoint))
                            .forEach(regions::add);
                }
                if (regions.isEmpty()) illegal(msgPrefix + "all eligible zone endpoints have 'enabled' set to 'false'");
                InstanceName instanceName = instance.map(InstanceName::from).get();
                for (RegionName region : regions) targets.add(new Target(region, instanceName, 1));
            }
        }

        if (zoneEndpointType.isEmpty())
            return Optional.of(new Endpoint(endpointId.orElse(Endpoint.DEFAULT_ID), containerId, level, targets));
        return Optional.empty();
    }
Reads the content of an (implicit or explicit) instance tag producing an instances step

@param instanceNameString a comma-separated list of the names of the instances this is for
@param instanceElement the element having the content of this instance
@param parentTag the parent of instanceTag (or the same, if this instance is implicitly defined, which means instanceTag is the root)
@return the instances specified, one for each instance name element
config-model-api/src/main/java/com/yahoo/config/application/api/xml/DeploymentSpecXmlReader.java