Goal

In this tutorial, we add support for custom properties using the LuciadFusion API.

As discussed in the Using custom properties with Data article, you add support for custom properties to LuciadFusion by plugging in a custom property decoder. In this tutorial, we create such a custom property decoder and make it available to LuciadFusion.

The example

In this tutorial we define two custom properties:

  • status : the status of the data.

  • license : the license applied to the data.

We assume that the license of a data resource is stored in a file that has the same name as the data source, but has a .license extension. For instance, if we have a data file /data/world.shp, the file holding the license is /data/world.license. We don’t define a custom metadata decoder, which means that our custom property decoder has to search and read that file by itself.

The status property comes from the ISO metadata generated by LuciadFusion.

Create a custom property decoder

Creating a custom property decoder comes down to implementing the ILfnCustomPropertyDecoder interface.

public class CustomPropertyDecoderImpl implements ILfnCustomPropertyDecoder {
}

The ILfnCustomPropertyDecoder interface has two methods that we need to implement:

  • getPropertyDefinitions: here we define the possible set of custom properties.

  • decode: here we decode the custom property values

Define the custom properties

We start by defining our custom properties. To do this, we implement the getPropertyDefinitions method. There we define a name and type for every possible property. Optionally we can define a display name and mark the property as queryable, meaning that we can use it in expressions when querying for data.

While it’s not required to define all custom properties that the decoder can return, it’s highly recommended to do so. Undefined properties don’t have a display name and aren’t queryable. In addition, LuciadFusion won’t be able to verify that the type of decoded custom properties matches the type of its definition.

In our example, we define two properties. The 'status' property, which is defined by ISO 19115, has a fixed set of possible values. That’s why we define it as an enumeration. We want to be able to search for data with a specific status, so we mark that property as queryable. The 'license' property is a regular string property containing the license applied to the data. This is typically a lot of text which we won’t use in query expressions, so we mark that property as non-queryable.

This method returns a TLfnCustomPropertyDefinitions object, which we can create using a TLfnCustomPropertyDefinitions.Builder. We add the custom properties to this builder one by one. For every property, we must specify at least its name and type. In addition, we can give it a display name, and we can also mark this property as queryable. Calling the build() method at the end creates the TLfnCustomPropertyDefinitions object with all defined custom properties.

Define the custom properties
private static final String STATUS_PROPERTY = "status";
private static final String LICENSE_PROPERTY = "license";

private static final Set<String> STATUS_VALUES = new HashSet<>(Arrays.asList(
    "completed", "obsolete", "planned", "required", "historicalArchive", "onGoing", "underDevelopment"
));

@Override
public TLfnCustomPropertyDefinitions getPropertyDefinitions() {
  return TLfnCustomPropertyDefinitions.newBuilder()
                                      .addEnumProperty(STATUS_PROPERTY, "Status", STATUS_VALUES, true)
                                      .addStringProperty(LICENSE_PROPERTY, "License", false)
                                      .build();
}

Decode the custom properties

Next, we implement the decode method. To make the implementation easier to read, we create two helper methods first: one for decoding the status property, and another one for decoding the license property.

We start by decoding the 'status' property from a TLcdISO19115Metadata object.

Decode the status property
/**
 * Decodes the 'status' property from the ISO metadata.
 */
private TLfnCustomProperty decodeStatus(TLcdISO19115Metadata aMetadata) {
  List<TLcdISO19115Identification> identificationInfo = aMetadata.getIdentificationInfo();
  if (identificationInfo != null && !identificationInfo.isEmpty()) {
    List<TLcdISO19115ProgressCode> statusCodes = identificationInfo.get(0).getStatus();
    if (statusCodes != null && !statusCodes.isEmpty()) {
      String status = statusCodes.get(0).getValueObject();
      return TLfnCustomProperty.newBuilder().name(STATUS_PROPERTY).stringValue(status).build();
    }
  }
  return null;
}

The 'license' property isn’t in a TLcdISO19115Metadata object, but in a file next to the data file. The name of the file holding the license is the same as the name of the data file, but with a .license extension. So, to decode this 'license' property, we first search for this file. If it exists, we read it and return the content as the 'license' property. This method returns the license property itself, and the location of the license file too.

Decode the license property
/**
 * Decodes the 'license' property from the license file located next to the data file itself.
 */
private TLcdPair<TLfnCustomProperty, TLcdModelMetadata.Source> decodeLicense(String aSourceName) throws IOException {
  String licenseFileName = toLicenseFileName(aSourceName);
  if (licenseFileName == null) {
    return null;
  }

  String license;
  try (InputStream inputStream = Files.newInputStream(Paths.get(licenseFileName))) {
    license = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8))
        .lines()
        .collect(Collectors.joining("\n"));
  }

  return new TLcdPair<>(
      TLfnCustomProperty.newBuilder().name(LICENSE_PROPERTY).stringValue(license).build(),
      new TLcdModelMetadata.Source(licenseFileName, "text/plain")
  );
}

/**
 * Translates the given source name of the data file into the name of the '.license' file.
 */
private String toLicenseFileName(String aSourceName) {
  String baseName = aSourceName.substring(0, aSourceName.lastIndexOf('.'));
  String licenseFileName = baseName + ".license";

  // check if the property file exists
  if (Files.exists(Paths.get(licenseFileName))) {
    return licenseFileName;
  }

  return null;
}

With these helper methods, we can now implement the decode method.

Decode the custom properties
@Override
public CustomPropertiesWithSource decode(String aSourceName, TLcdISO19115Metadata aMetadata) throws IOException {
  List<TLfnCustomProperty> properties = new ArrayList<>();
  List<TLcdModelMetadata.Source> sources = new ArrayList<>();

  // decode the status property
  TLfnCustomProperty status = decodeStatus(aMetadata);
  if (status != null) {
    properties.add(status);
  }

  // decode the license property
  TLcdPair<TLfnCustomProperty, TLcdModelMetadata.Source> license = decodeLicense(aSourceName);
  if (license != null) {
    properties.add(license.getKey());
    sources.add(license.getValue());
  }

  return new CustomPropertiesWithSource(properties, sources);
}

Spring integration

Now we’ve created the class that defines and decodes the custom properties, we need to make sure that LuciadFusion can pick it up.

To do so, we must configure a Spring bean for this class so that LuciadFusion can pick it up from the Spring application context. To configure the Spring bean, we create a class and mark it with the @Configuration annotation. In this class, we define a Spring bean for our implementation of ILfnCustomPropertyDecoder. Make sure to add the package of this class to the Spring application context using the additionalScanPackages property. Set this property in the config/fusion.common.yml file, or create a new Spring profile.

Define the Spring bean for the custom property decoder
@Configuration
public class CustomPropertiesConfiguration {

  @Bean
  public ILfnCustomPropertyDecoder customPropertyDecoder() {
    return new CustomPropertyDecoderImpl();
  }

}

Conclusion

In this tutorial, we illustrated how to define and decode custom properties. For more information about accessing custom properties and searching for data items based on custom property values, see the Using custom properties with Data article.