Generated metadata constant field of wrong class

Hi there LEAF team,

We have a DynamicLookupListField in one of our types. The field metadata in the constants generated for this field however is StringFieldIdentifier.

Our XML for the type has a pattern like this:

<type type="Car" edition="1" >
    <LookupListField            propName="make"
                                shortName="Make"
                                required="true"
                                listName="Makes"
                                listEdition="1" />

    <DynamicLookupListField     propName="model"
                                parentPropName="make" >
        <subLists>
            <subList subListName="ToyotaModels" parentListValue="Toyota" />
            <subList subListName="HondaModels" parentListValue="Honda" />
            <subList subListName="NissanModels" parentListValue="Nissan" />
        </subLists>
    </DynamicLookupListField>
</type>

However, the generated metadata constants look like this:

/**
 * This class was generated by the LEAF Metadata Parser. DO NOT EDIT.
 */
public final class CarMetadataConstants {

    private CarMetadataConstants() {}

    // Type Name Constant
    public static final String TYPE = "Car";

    // Prop Name Constants
    public static final StringFieldIdentifier MAKE = new StringFieldIdentifier("make");
    public static final StringFieldIdentifier MODEL = new StringFieldIdentifier("model");
}

In this instance I was hoping that MODEL would be a DynamicLookupListFieldMetadata so that I can make use of that mapping of make to available models.

We’re using LEAF version 3.1.35

Do we know if this is fixed in a later version of LEAF? Is there perhaps something I’ve misconfigured with the code generation?

For what it is worth in our particular use case the Car type is not concrete, but is instead inherited by a couple of other types.

But the metadata still lives in a constant class for the “parent” in this situation.

The type-safe identifiers the MetadataConstantsParser task generates are specifically constants that encapsulate the propName and expected value class of some FieldMetadata. Because a DynamicLookupListFieldMetadata’s value class is String, the generated identifier should be String. The main reason they exist isn’t to provide extra info about that FieldMetadata, such as what are the possible values of a LookupList field or Dynamic LookupList field, but rather a type-safe way to put values into a DataObject and to get them back out.

For example, if my LookupList field: state should determine the choices for my Dynamic LookupList field: city, then I could use the type-safe identifiers like this to set DataObject values for these fields and to access them.

DataObject person = DataObject.newBuilder()
        .withType(PersonMetadataConstants.TYPE)
        .with(PersonMetadataConstants.STATE, "NY")
        .with(PersonMetadataConstants.CITY, "New York City")
        .build();
String state = person.get(PersonMetadataConstants.STATE);
String city = person.get(PersonMetadataConstants.CITY);
// The following wouldn't compile, hence the name type-safe identifiers:
//   int state = person.get(PersonMetadataConstants.STATE);
// and that's because STATE is a "String"FieldIdentifier not an "Integer" one.

See more about type-safe identifiers on this Knowledge Base article.

Programmatically if you need a way to find out what choices a user has available to them given the state they’ve picked, you could do something like this:

var context = Context.makeSystemContext();
var metadataService = dataService.getMetadataService();

BeanMetadata personBeanMetadata = getOnlyElement(metadataService.loadLatestBeanMetadataEditions(List.of("Person"), context));
var state = person.get(PersonMetadataConstants.STATE);

var cityFieldMetadata = (DynamicLookupListFieldMetadata) personBeanMetadata.getField("city");
NameAndEdition subListNameAndEdition = cityFieldMetadata.getSubLists()
        .stream()
        .filter(subList -> state.equals(subList.getParentListValue()))
        .collect(MoreCollectors.onlyElement())
        .getSubListNameAndEdition();

LookupList lookupList = getOnlyElement(dataService.getLookupListService()
        .loadByNameAndEditions(List.of(subListNameAndEdition), context));
List<String> possibleValues = lookupList.getItems()
        .stream()
        .map(LookupListItem::getValue)
        .collect(Collectors.toList());

And of course that’s a fair bit of code for that field, but the main use case is to populate dropdown menus in the UI, and the LEAF JS component for rendering the dropdown menu handles all of that logic for you.

All of that said, there is also another generated file for constants for LookupLists that includes their names and values. So you could certainly leverage it too. So if your States LookupList has values NY, PA, WV, etc., you would see a generated file like this somewhere:

public final class States_LookupListConstants {

    private States_LookupListConstants() {}

    // Name Constant
    public static final String NAME = "States";

    // Item Value Constants
    public static final String NY = "NY";
    public static final String PA = "PA";
    public static final String WV = "WV";

    // Constants Set
    public static final Set<String> VALUES = ImmutableSet.<String> builder()
            .add(NY)
            .add(PA)
            .add(WV)
            .build();
}

And of course you could write simple if-statements to mimic the logic above, like:

Set<String> possibleValues;
if (state.equals(States_LookupListConstants.NY)) {
    possibleValues = NYCities_LookupListConstants.VALUES;
}
// etc.

Note that the metadata Gradle plugin didn’t start generating LookupListConstants files until 3.5, but the LookupListConstantsParser can be manually wired up on earlier versions. There should be a KB page on how to do that.

Yeah, the logic of which parent value belongs to which sub-list is what we were after. I think the big thing for me is that I didn’t want to have to duplicate the logic that is already in the XML.

So this solution with the metadata service is just what we need, thank you!

2 Likes