All Downloads are FREE. Search and download functionalities are using the official Maven repository.

dk.mada.jaxrs.openapi.Resolver Maven / Gradle / Ivy

package dk.mada.jaxrs.openapi;

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.Optional;
import java.util.Set;
import java.util.function.Predicate;
import java.util.stream.Collectors;

import org.jspecify.annotations.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import dk.mada.jaxrs.model.Dto;
import dk.mada.jaxrs.model.Property;
import dk.mada.jaxrs.model.Validation;
import dk.mada.jaxrs.model.api.Content;
import dk.mada.jaxrs.model.api.Operation;
import dk.mada.jaxrs.model.api.Operations;
import dk.mada.jaxrs.model.api.Parameter;
import dk.mada.jaxrs.model.api.RequestBody;
import dk.mada.jaxrs.model.api.Response;
import dk.mada.jaxrs.model.types.Reference;
import dk.mada.jaxrs.model.types.Type;
import dk.mada.jaxrs.model.types.TypeArray;
import dk.mada.jaxrs.model.types.TypeInterface;
import dk.mada.jaxrs.model.types.TypeMap;
import dk.mada.jaxrs.model.types.TypeName;
import dk.mada.jaxrs.model.types.TypeNames;
import dk.mada.jaxrs.model.types.TypeReference;
import dk.mada.jaxrs.model.types.TypeSet;
import dk.mada.jaxrs.model.types.TypeValidation;
import dk.mada.jaxrs.model.types.TypeVoid;

/**
 * Works through a parsed model that contains parser type references and resolves them to pure model types.
 */
public final class Resolver {
    private static final Logger logger = LoggerFactory.getLogger(Resolver.class);

    /** The unknown type. */
    private static final TypeUnknownAtParseTime UNKNOWN_TYPE = TypeUnknownAtParseTime.get();
    /** Parser types to their dereferenced model type. */
    private final Map dereferencedTypes = new HashMap<>();

    /** The parser options. */
    private final ParserOpts parserOpts;
    /** Type names. */
    private final TypeNames typeNames;
    /** Types from parsing. */
    private final ParserTypes parserTypes;
    /** Conflict renamer. */
    private final ConflictRenamer conflictRenamer;
    /** Configuration to abort on resolver failure. */
    private final boolean abortOnResolverFailure;
    /** Configuration to fixup missing type. */
    private final boolean fixupMissingType;
    /** Dto-to-property-names that need validation to be relaxed. */
    private final Map> dtoPropertiesToBeRelaxed = new HashMap<>();

    /**
     * Create new instance.
     *
     * @param parserOpts      the parser options
     * @param typeNames       the type names instance
     * @param parserTypes     the types collected during parsing
     * @param conflictRenamer the conflict renamer
     */
    public Resolver(ParserOpts parserOpts, TypeNames typeNames, ParserTypes parserTypes, ConflictRenamer conflictRenamer) {
        this.parserOpts = parserOpts;
        this.typeNames = typeNames;
        this.parserTypes = parserTypes;
        this.conflictRenamer = conflictRenamer;

        abortOnResolverFailure = parserOpts.isAbortOnResolverFailure();
        fixupMissingType = parserOpts.isFixupMissingType();
    }

    /**
     * Converts the ParserTypes into finally resolved and mapped DTOs for the model.
     *
     * First the DTOs are renamed (if necessary) to resolve name conflicts. Then references to DTOs are resolved, changing
     * parser- references to model-references.
     *
     * Note that this updates the resolver/ParserTypes state with renames and type mappings.
     *
     * @return DTOs for the model
     */
    public List getDtos() {
        Set unresolvedDtos = parserTypes.getActiveDtos();

        if (logger.isDebugEnabled()) {
            logger.debug("= Parsed DTOs:");
            unresolvedDtos.stream()
                    .forEach(dto -> logger.debug(" - {}:{}", dto.openapiId(), dto.name()));
        }

        Collection withoutTypeRefDtos = loopedDtoRemapping("typeRef", unresolvedDtos, Resolver::isDtoReferenceOnly, false);
        Collection withoutPrimitiveDtos = loopedDtoRemapping("primitive", withoutTypeRefDtos, Resolver::isDtoPrimitiveWrapperOnly,
                !parserOpts.isMapSimpleDtosToObject());
        Collection withoutModelTypes = loopedDtoRemapping("model types", withoutPrimitiveDtos, this::isDtoModelType, false);

        Collection filteredDtos = withoutModelTypes;

        if (logger.isDebugEnabled()) {
            logger.debug("= Filtered DTOs:");
            filteredDtos.stream()
                    .forEach(dto -> logger.debug(" - {}:{}", dto.openapiId(), dto.name()));
        }

        // Rename DTOs as a separate pass so there are stable
        // targets for dereferencing during the resolve pass.
        Collection renamedDtos = conflictRenamer.resolveNameConflicts(filteredDtos);

        Collection expandedDtos = extractCompositeDtos(renamedDtos);

        Collection foldedDtos = foldInheritance(expandedDtos);

        List dereferencedDtos = dereferenceDtos(foldedDtos);

        if (logger.isDebugEnabled()) {
            logger.debug("= Resolved DTOs:");
            dereferencedDtos.stream()
                    .sorted((a, b) -> a.name().compareToIgnoreCase(b.name()))
                    .forEach(d -> logger.info(" - {}", d));
        }

        return dereferencedDtos;
    }

    /**
     * Remaps DTOs in a loop as long as there are changes.
     *
     * @param title  the title of the remapping
     * @param dtos   the DTOs to remap
     * @param filter the filter to apply to the DTOs
     * @param skip   flag to skip the step
     * @return the (remaining) remapped DTOs
     */
    private Collection loopedDtoRemapping(String title, Collection dtos, Predicate filter, boolean skip) {
        Collection output = dtos;

        if (skip) {
            logger.debug("== DTO filtering {} - skipped", title);
            return output;
        }

        logger.debug("== DTO filtering {}", title);

        boolean runAnotherPass;
        int pass = 1;
        do {
            logger.debug(" {} pass {} with {} dtos", title, pass, output.size());

            List updated = output.stream()
                    .filter(dto -> applyDtoFilter(filter, dto))
                    .toList();
            runAnotherPass = output.size() != updated.size();
            output = updated;
            pass++;
        } while (runAnotherPass);

        logger.debug(" completed {} DTO remapping with {}->{} dtos", title, dtos.size(), output.size());
        return output;
    }

    private boolean applyDtoFilter(Predicate filter, Dto dto) {
        String name = dto.name();
        logger.trace(" - {} {}", name, dto);

        if (filter.test(dto)) {
            TypeReference ref = resolve(dto.reference());
            Type newType = parserTypes.remapDto(dto.typeName(), ref);
            logger.trace("   : remap {} to {}", name, newType);
            return false;
        }

        logger.trace("   : keep {}", name);
        return true;
    }

    /**
     * It is valid for type references (just a plain $ref) to be represented in OpenApi documents, but there is no good way
     * to express it in Java (unless you want to consider extension).
     *
     * So replace these DTOs with whatever they point to.
     *
     * If the DTO points to itself, abort parsing.
     *
     * @param dto the DTO to consider
     * @return true if this DTO is only a reference to some other DTO
     */
    private static boolean isDtoReferenceOnly(Dto dto) {
        boolean isRefOnly = dto.reference() != null
                && (dto.reference().refType() == UNKNOWN_TYPE || dto.reference().isDto())
                && dto.properties().isEmpty()
                && !dto.isEnum()
                && dto.implementsInterfaces().isEmpty()
                && !dto.subtypeSelector().isPresent()
                && dto.extendsParents().isEmpty();

        if (isRefOnly
                && dto.reference() instanceof ParserTypeRef ptr
                && ptr.refTypeName().equals(dto.openapiId())) {
            throw new IllegalArgumentException("DTO " + dto.openapiId().name() + " references itself directly!?");
        }
        return isRefOnly;
    }

    /**
     * It is valid for simple (non-Object) types to be represented in OpenApi documents as standalone DTOs. But there is no
     * good way to express it in Java.
     *
     * So filter out these DTOs, replacing them with whatever they point to. This will result in the types being represented
     * as property fields instead.
     *
     * TODO: this and isDtoModelType should be reworked to carry a new TypeDef object into the model. And then let the
     * generator decide if these should be inlined (so basically move this pass down).
     *
     * @param dto the DTO to consider
     * @return true if the DTO is a primitive (not object)
     */
    private static boolean isDtoPrimitiveWrapperOnly(Dto dto) {
        Type dtoType = dto.reference().refType();
        return !dto.isEnum()
                && (dtoType.isPrimitive() || dtoType.isPlainObject());
    }

    /**
     * Filter out DTOs that are of a known model type.
     *
     * TODO: see isDtoPrimitiveWrapperOnly TODO: this predicate clearly shows a lot of types/options *not* covered by tests
     *
     * @param dto the DTO to consider
     * @return true if the DTO is a known model type.
     */
    private boolean isDtoModelType(Dto dto) {
        Type dtoType = dto.reference().refType();
        return (dtoType.isDate() && parserOpts.isJseLocalDate())
                || (dtoType.isDateTime()
                        && (parserOpts.isJseLocalDateTime() || parserOpts.isJseOffsetDateTime() || parserOpts.isJseZonedDateTime()))
                || (dtoType.isTime() && parserOpts.isJseLocalTime())
                || (dtoType.isUUID() && parserOpts.isJseUUID());
    }

    private Collection extractCompositeDtos(Collection dtos) {
        logger.debug("Look for composite DTOs");
        return dtos.stream()
                .map(dto -> extractIfCompositeDto(dto, dtos))
                .toList();
    }

    private Dto extractIfCompositeDto(Dto dto, Collection dtos) {
        Type type = dto.reference().refType();

        if (type instanceof ParserTypeComposite tc) {
            return extractCompositeDto(dtos, dto, tc);
        }
        if (type instanceof ParserTypeCombined tc) {
            return extractCombinedDto(dtos, dto, tc);
        }

        return dto;
    }

    /**
     * Extract composite Dto information.
     *
     * A composite DTOs (schema is allOf) has the (assumed) Dto types it expands declared as name references in the type.
     *
     * Now that parsing is complete, all Dto types are known.
     *
     * So find the referenced Dtos and store it in directly in the Dto's data.
     *
     * The Dto's type is replaced later during dereferencing (since all the information captured in ParserTypeComposite has
     * now been moved into the Dto model).
     *
     * @param dtos all the known Dtos.
     * @param dto  the Dto to store data in
     * @param tc   tge composite type information
     * @return the updated dto
     */
    private Dto extractCompositeDto(Collection dtos, Dto dto, ParserTypeComposite tc) {
        String openapiName = dto.openapiId().name();
        logger.debug(" - expand composite DTO {}", openapiName);
        logger.debug(" tc: contains: {}", tc.containsTypes());
        logger.debug(" tc: external: {}", tc.externalDtoReferences());

        List externalDtos = tc.externalDtoReferences().stream()
                .map(tn -> getDtoWithOpenapiId(dtos, tn))
                .toList();

        if (logger.isDebugEnabled()) {
            List extendsNames = externalDtos.stream()
                    .map(Dto::name)
                    .sorted()
                    .toList();
            logger.debug("    extends {}", extendsNames);
        }

        return Dto.builderFrom(dto)
                .extendsParents(externalDtos)
                .build();
    }

    /**
     * Extract composite Dto information.
     *
     * A composite DTOs (schema is allOf) has the (assumed) Dto types it expands declared as name references in the type.
     *
     * Now that parsing is complete, all Dto types are known.
     *
     * So find the referenced Dtos and store it in directly in the Dto's data.
     *
     * The Dto's type is replaced later during dereferencing (since all the information captured in ParserTypeComposite has
     * now been moved into the Dto model).
     *
     * @param dtos all the known Dtos.
     * @param dto  the Dto to store data in
     * @param tc   tge composite type information
     * @return the updated dto
     */
    private Dto extractCombinedDto(Collection dtos, Dto dto, ParserTypeCombined tc) {
        String openapiName = dto.openapiId().name();
        logger.debug(" - expand combined DTO {}", openapiName);

        List combinesDtos = tc.externalDtoReferences().stream()
                .map(tn -> getDtoWithOpenapiId(dtos, tn))
                .toList();

        if (logger.isDebugEnabled()) {
            List combinesNames = combinesDtos.stream()
                    .map(Dto::name)
                    .sorted()
                    .toList();
            logger.debug("    combines {}", combinesNames);
        }

        List combinedProps = combinesDtos.stream()
                .flatMap(d -> d.properties().stream())
                .sorted((a, b) -> a.name().compareTo(b.name()))
                .toList();

        Set seenPropertyNames = new HashSet<>();
        List selectedProps = combinedProps.stream()
                .filter(p -> seenPropertyNames.add(p.name()))
                .toList();

        // Remember the property names for the resolver so their
        // validation requirements can be relaxed.
        Set propNamesNeedingRelaxation = selectedProps.stream()
                .map(Property::name)
                .collect(Collectors.toSet());

        dtoPropertiesToBeRelaxed.put(dto.typeName(), propNamesNeedingRelaxation);

        return Dto.builderFrom(dto)
                .properties(selectedProps)
                .build();
    }

    private Dto getDtoWithOpenapiId(Collection dtos, TypeName tn) {
        return dtos.stream()
                .filter(d -> d.openapiId().equals(tn))
                .findFirst()
                .orElseThrow(() -> new IllegalStateException("Did not find referenced DTO " + tn));
    }

    /**
     * This dereferences the name-based parser-type-references into the actual target types.
     *
     * @param dtos the collection of DTOs still using ptrs
     * @return dereferenced DTOs
     */
    private List dereferenceDtos(Collection dtos) {
        logger.debug("==== Dereference DTOs");
        return dtos.stream()
                .map(this::derefDto)
                .toList();
    }

    /**
     * Reconstructs the inheritance between DTOs as expressed by oneof-discriminator information.
     *
     * This removes fields from sub-classes that are also present in the super class. Without any form of validation though.
     *
     * @param dtos the DTOs containing discriminator information
     * @return dtos with inheritance information
     */
    private List foldInheritance(Collection dtos) {
        logger.debug("Look for DTO implements");
        Map dtosWithSuper = new HashMap<>();

        for (Dto dto : dtos) {
            dto.subtypeSelector().ifPresent(subtypes -> {
                for (Reference r : subtypes.typeMapping().values()) {
                    dtosWithSuper.put(r.typeName(), dto);
                }
            });
        }

        return dtos.stream()
                .map(dto -> adjustToParentExtension(dto, dtosWithSuper.get(dto.typeName())))
                .toList();
    }

    /**
     * Change DTO if it extends a parent.
     *
     * Make a link to the parent and remove inherited properties.
     *
     * @param dto    the dto to change
     * @param parent the parent dto, or null
     * @return the updated dto
     */
    private Dto adjustToParentExtension(Dto dto, @Nullable Dto parent) {
        if (parent == null) {
            return dto;
        }

        String parentName = parent.name();
        String dtoName = dto.name();
        logger.debug(" {} extends {}", dtoName, parentName);
        List localProperties = dto.properties()
                .stream()
                .filter(dtoProperty -> isLocalToDto(parent, dtoProperty.name()))
                .toList();

        ArrayList newParents = new ArrayList<>(dto.extendsParents());
        newParents.add(parent);

        return Dto.builderFrom(dto)
                .extendsParents(newParents)
                .properties(localProperties)
                .build();
    }

    private boolean isLocalToDto(Dto parent, String propertyName) {
        return parent.properties().stream()
                .noneMatch(prop -> propertyName.equals(prop.name()));
    }

    private Dto derefDto(Dto dto) {
        Reference dtoTypeRef = dto.reference();

        String name = dto.name();
        TypeName typeName = dto.typeName();

        List resolvedParents = dto.extendsParents().stream()
                .map(this::derefDto)
                .toList();

        List implementsInterfaces = parserTypes.getInterfacesImplementedBy(typeName);
        logger.debug(" - deref DTO {} : {}", name, dtoTypeRef);
        logger.debug(" - implements: {}", implementsInterfaces);
        return Dto.builderFrom(dto)
                .extendsParents(resolvedParents)
                .reference(resolve(dtoTypeRef))
                .properties(derefProperties(dto))
                .implementsInterfaces(implementsInterfaces)
                .build();
    }

    private List derefProperties(Dto dto) {
        return dto.properties().stream()
                .map(p -> derefProperty(dto, p))
                .toList();
    }

    private Property derefProperty(Dto parent, Property property) {
        String propName = property.name();
        logger.debug("    prop: {}", propName);
        TypeReference resolvedRef = resolve(property.reference());
        Validation resolvedValidation = property.validation();

        String location = parent.name() + ":" + propName;
        resolvedRef = assertOrFixupActualType(resolvedRef, resolvedValidation, location);

        // A Map's inner type may be undefined, try to fix it
        if (resolvedRef.refType() instanceof TypeMap map) {
            Type innerType = map.innerType();
            if (innerType instanceof TypeReference innerRef) {
                // Not resolving inner type - happens in depth by resolve() on the property
                TypeReference newInnerType = assertOrFixupActualType(innerRef, innerRef.validation(), location + " (map's value)");
                if (!newInnerType.equals(innerRef)) {
                    TypeMap newMap = TypeMap.of(map.typeNames(), newInnerType);
                    resolvedRef = TypeReference.of(newMap, resolvedRef.validation());
                }
            }
        }

        // The type may provide validation - use that if there is none on the property
        // TODO: If the property has validation, it should probably be attempted
        // merged with that of the type. If they conflict, fail (with some config to ignore)
        if (resolvedValidation.isEmptyValidation()) {
            resolvedValidation = resolvedRef.validation();
        } else if (resolvedValidation == Validation.REQUIRED_VALIDATION) {
            // handle simple required_validation since it is simplest - and probably enough for now
            resolvedValidation = Validation.builder().from(resolvedRef.validation())
                    .isRequired(true)
                    .build();
        }

        // allOf constructed DTOs need to be able to deserialize subsets
        // of their properties, so this part relaxes the validation
        // requirements for such properties.
        // This could probably be stricter, but I am unsure about the proper
        // semantics of such constructs.
        Set relaxProps = dtoPropertiesToBeRelaxed.get(parent.typeName());
        if (relaxProps != null && relaxProps.contains(propName)) {
            logger.trace("     + relaxing validation");
            resolvedValidation = Validation.builder().from(resolvedValidation)
                    .isNullable(true)
                    .isRequired(false)
                    .build();
        }

        // TODO: get example from type, see mada.tests.e2e.regression.string_pattern.dto.KlarTilBeslutningsGrundlagResponse
        // is should have:
        // @Schema(required = true, example = "2022-02-18-09.18.12.788990")
        Optional resolvedExample = property.example();

        logger.debug("    deref prop {}\n     from: {}\n           {}\n     to: {}\n         {}",
                propName,
                property.reference(), property.validation(),
                resolvedRef, resolvedValidation);
        return Property.builder().from(property)
                .example(resolvedExample)
                .reference(resolvedRef)
                .validation(resolvedValidation)
                .build();
    }

    /**
     * Assert on or fixup undeclared types.
     *
     * Terminate parsing if type is "just" validation. This happens if no type is provided in the OpenApi document. This
     * check should live in resolve(), but then the error cannot provide location hints.
     *
     * @param typeRef    the type reference
     * @param validation the validation of the type reference
     * @param location   location to print in case of failure
     * @return the input type is valid, or a fallback type if so configured
     */
    private TypeReference assertOrFixupActualType(TypeReference typeRef, Validation validation, String location) {
        if (!(typeRef.refType() instanceof TypeValidation)) {
            return typeRef;
        }

        if (fixupMissingType) {
            logger.warn("Assuming type Object for {}", location);
            return resolve(ParserTypeRef.of(TypeNames.OBJECT, validation));
        } else {
            throw new IllegalArgumentException(
                    "Property " + location + " has no type! Set " + ParserOpts.PARSER_FIXUP_MISSING_TYPE + "=true to assume Object");
        }
    }

    /**
     * Resolves parser type references in operations.
     *
     * @param ops the operations to resolve types in
     * @return operations with pure model types
     */
    public Operations operations(Operations ops) {
        List dereferenced = ops.getAll().stream()
                .map(this::derefOp)
                .toList();
        return new Operations(dereferenced);
    }

    private Operation derefOp(Operation op) {
        return Operation.builder().from(op)
                .responses(derefResponses(op.responses()))
                .parameters(derefParams(op.parameters()))
                .requestBody(op.requestBody().map(this::derefRequestBody))
                .build();
    }

    private RequestBody derefRequestBody(RequestBody requestBody) {
        return RequestBody.builder().from(requestBody)
                .content(derefContent(requestBody.content()))
                .formParameters(derefParams(requestBody.formParameters()))
                .build();
    }

    private List derefParams(List parameters) {
        return parameters.stream()
                .map(this::derefParam)
                .toList();
    }

    private Parameter derefParam(Parameter param) {
        return Parameter.builder().from(param)
                .reference(resolve(param.reference()))
                .build();
    }

    private List derefResponses(List responses) {
        return responses.stream()
                .map(this::derefResponse)
                .toList();
    }

    private Response derefResponse(Response response) {
        return Response.builder().from(response)
                .content(derefContent(response.content()))
                .build();
    }

    private Content derefContent(Content content) {
        return Content.builder().from(content)
                .reference(resolve(content.reference()))
                .build();
    }

    private TypeReference resolve(Reference ref) {
        TypeReference res;
        if (ref instanceof ParserTypeRef ptr) {
            res = resolve(ptr);
        } else if (ref instanceof TypeReference tr) {
            res = tr;
        } else {
            throw new IllegalStateException("Unhandled reference type " + ref.getClass());
        }

        // Remove empty validation chains
        while (res.validation().isEmptyValidation()
                && res.refType() instanceof TypeReference inner) {
            res = inner;
        }

        return res;
    }

    /**
     * Resolve parser references into model references.
     *
     * All incoming references may point to the initially parsed DTO instances. All returned references point to the final
     * model DTO instances that have been renamed as required.
     *
     * @param ptr the reference to resolve
     * @return the model reference
     */
    private TypeReference resolve(ParserTypeRef ptr) {
        Type t = ptr.refType() == UNKNOWN_TYPE ? parserTypes.get(ptr.refTypeName()) : ptr.refType();

        // Terminate parsing early if the type cannot be resolve
        // so nobody gets confused at compile time instead
        if (abortOnResolverFailure && t == UNKNOWN_TYPE) {
            throw new IllegalStateException(
                    "Failed to resolve a pointer:\n " + ptr + "\nThis is probably a bug in openapi-jaxrs-client - please report!");
        }

        Type resolvedT = resolveInner(t);
        TypeReference res = dereferencedTypes.computeIfAbsent(ptr, p -> TypeReference.of(resolvedT, ptr.validation()));

        logger.debug("  resolve {} -> {}", ptr, res);
        return res;
    }

    /**
     * Inner-most resolve of types (that can be parsed DTOs) to model types. May call itself recursively.
     *
     * @param type the type to resolve
     * @return the final model type
     */
    private Type resolveInner(Type type) {
        logger.trace(" resolveInner {}", type);
        if (type instanceof Dto dto) {
            return resolveDto(dto);
        } else if (type instanceof ParserTypeComposite ptc) {
            return resolveCompositeDto(ptc);
        } else if (type instanceof ParserTypeCombined ptc) {
            return resolveCombinedDto(ptc);
        } else if (type instanceof ParserTypeRef ptr) {
            return resolve(ptr);
        } else if (type instanceof TypeVoid) {
            return type;
        } else if (type instanceof TypeArray ta) {
            Type it = ta.innerType();
            Type newIt = resolveInner(it);
            logger.trace(" array {} -> {}", it, newIt);
            return TypeArray.of(typeNames, newIt);
        } else if (type instanceof TypeSet ts) {
            Type it = ts.innerType();
            Type newIt = resolveInner(it);
            logger.trace(" set {} -> {}", it, newIt);
            return TypeSet.of(typeNames, newIt);
        } else if (type instanceof TypeMap tm) {
            Type it = tm.innerType();
            Type newIt = resolveInner(it);
            logger.trace(" map {} -> {}", it, newIt);
            return TypeMap.of(typeNames, newIt);
        } else if (type instanceof TypeReference tr) {
            logger.info("FIXME:  XXXX should probably be removed / see tr {}", tr);
            if (tr.validation().isEmptyValidation()) {
                Type refType = tr.refType();
                if (refType instanceof TypeReference) {
                    logger.debug(" flatten empty typeref {} -> {}", tr, refType);
                    return refType;
                }
            }
            return type;
        } else {
            logger.trace("NOT dereferencing {}", type);
            return type;
        }
    }

    /**
     * Resolves a composite DTO reference.
     *
     * The composite reference contains local properties and/or external DTO references. These have all been moved into the
     * Dto object in expandCompositeDtos.
     *
     * @param ptc the composite parser type
     * @return the simplified object reference
     */
    private TypeReference resolveCompositeDto(ParserTypeComposite ptc) {
        TypeName dtoName = ptc.typeName();
        // There is no reference to the real DTO type, but it does not matter, as the name
        // lookup is guaranteed to find it.
        Dto dto = null;
        return resolveDto(dtoName, dto);
    }

    /**
     * Resolves a combined DTO reference.
     *
     * The combined reference contains local properties and/or external DTO references. These have all been moved into the
     * Dto object in expandCombinedDtos.
     *
     * @param ptc the combined parser type
     * @return the simplified object reference
     */
    private TypeReference resolveCombinedDto(ParserTypeCombined ptc) {
        TypeName dtoName = ptc.typeName();
        // There is no reference to the real DTO type, but it does not matter, as the name
        // lookup is guaranteed to find it.
        Dto dto = null;
        return resolveDto(dtoName, dto);
    }

    /**
     * Resolve a Dto.
     *
     * The Dto may have been mapped to another types. If so, the replacement happens here.
     *
     * @param dto the dto to possibly replace
     * @return the final type reference for the Dto
     */
    private TypeReference resolveDto(Dto dto) {
        return resolveDto(dto.typeName(), dto);
    }

    private TypeReference resolveDto(TypeName dtoName, @Nullable Type fallbackDto) {
        // See if DTO has been remapped to something else
        Type remappedDto = parserTypes.find(dtoName).orElse(fallbackDto);
        if (remappedDto == null) {
            throw new IllegalStateException("Did not find a dto named " + dtoName);
        }

        // Note: catches only 2-level remapped dtos. This is enough for now, but
        // may need to loop
        if (remappedDto instanceof Dto dto) {
            remappedDto = conflictRenamer.getConflictRenamedDto(dto);
        }

        // Convert parser DTO instance to model DTO instance
        // Wrap in a reference - or cyclic DTOs will not be possible
        return TypeReference.of(remappedDto, Validation.NO_VALIDATION);
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy