Many parts of a Geode cache contain configuration details that are persisted through server restarts or applied to new servers upon startup, such as region attributes and async event queues. If a developer wants to add persistence to a part of the cache that does not currently have any persisted configuration, one way to do so is via the CacheService interface. Services created using this interface are loaded during cache initialization using the Java ServiceLoader and can be retrieved from the cache by calling Cache.getService(*YOUR_INTERFACE.CLASS*)

The service is initialized with some default state and can then be configured during the CacheCreation.create() method using configuration details parsed from XML. The current state of the service can also be persisted to XML when it is modified via gfsh commands. The following steps outline how to achieve this.

Step-by-step guide:

  1. Create an .xsd file in resources/META-INF/schemas/geode.apache.org/schema/*PACKAGE_NAME*/*SCHEMA_NAME*.xsd, specifying the targetNamespace as geode.apache.org/schema/*PACKAGE_NAME*and define the elements of your service that you wish to persist.

    Example .xsd file
    <?xml version="1.0" encoding="UTF-8"?>
    
    <xsd:schema
        targetNamespace="http://geode.apache.org/schema/*PACKAGE_NAME*"
        xmlns:xsd="http://www.w3.org/2001/XMLSchema"
        elementFormDefault="qualified"
        attributeFormDefault="unqualified"
        version="1.0">
    
    ***SCHEMA_ELEMENTS_DEFINITION***
    
    </xsd:schema>
  2. Create an XML parser for your service which extends AbstractXmlParser. The getNamespaceUri() method should return the same namespace that was defined as the targetNamespace in your schema. The startElement() and endElement() methods should be implemented to process each element of your schema by interacting with the AbstractXmlParser.stack field via peek(), pop() and push() to navigate through the parsed XML elements. This can be implemented directly in your XML parser class (see LuceneXmlParser) or by delegating the process to an ElementType enum with enumerators for each element defined in your XSD schema (see JdbcConnectorServiceXmlParser). You may also need to create data structure classes that hold the configuration information parsed from the XML until the cache has been created and the service you want to configure exists. These classes are typically named *Creation. Ultimately you will be adding an object/Creation generated from your top-level XML element to the CacheCreation class and configuring your service in the CacheCreation.create() method using the information contained in that object.

    Example XML parser not using ElementType
    public class ExampleXmlParser extends AbstractXmlParser {
    
      public static final String NAMESPACE = "http://geode.apache.org/schema/*PACKAGE_NAME*"
      public static final String *PARENT_NAME* = *SCHEMA_ELEMENT_NAME*
      public static final String *NESTED_NAME* = *NESTED_SCHEMA_ELEMENT_NAME*
      
    @Override
      public String getNamespaceUri() {
        return NAMESPACE;
      }
    
      @Override
      public void startElement(String uri, String localName, String qName, Attributes atts)
          throws SAXException {
    
        if (!NAMESPACE.equals(uri)) {
          return;
        }
        if (*PARENT_NAME*.equals(localName)) {
          startParent(atts);
        }
        if (*NESTED_NAME*.equals(localName)) {
          startNested(atts);
        }
      }
    
      private void startParent(Attributes atts) {
        if (!(stack.peek() instanceof CacheCreation)) {
          throw new CacheXmlException("<" + *PARENT_NAME* + "> elements must occur within <cache> elements");
        }
        **create and initialize your parent Creation object and push it to the stack**
      }
      
      private void startNested(Attributes atts) {
        if (!(stack.peek() instanceof *PARENT_OBJECT_CREATION*)) {
          throw new CacheXmlException("<" + *NESTED_NAME* + "> elements must occur within <" + *PARENT_NAME* + "> elements");
        }
        **create and initialize your nested object and add it to its parent Creation object** 
      }
     
      @Override
      public void endElement(String uri, String localName, String qName) throws SAXException {
        if (!NAMESPACE.equals(uri)) {
          return;
        }
        if (*PARENT_NAME*.equals(localName)) {
          endParent();
        }
    
        if (*NESTED_NAME*.equals(localName)) {
          endNested();
        }
      }
    
      private void endParent() {
        **pop your object off the top of the stack, then add it to the CacheCreation object retrieved using stack.peek()**
      }
      
      private void endNested() {
        // Do nothing, since the nested object has already been added to 
        // the parent object Creation
      }
    
    
    Example XML parser using ElementType pattern
    public class ExampleXmlParser extends AbstractXmlParser {
    
      public static final String NAMESPACE = "http://geode.apache.org/schema/*PACKAGE_NAME*"
      public static final String *PARENT_NAME* = *SCHEMA_ELEMENT_NAME*
      public static final String *NESTED_NAME* = *NESTED_SCHEMA_ELEMENT_NAME*
    
      @Override
      public String getNamespaceUri() {
        return NAMESPACE;
      }
    
      @Override
      public void startElement(String uri, String localName, String qName, Attributes attributes)
          throws SAXException {
        if (!NAMESPACE.equals(uri)) {
          return;
        }
        ElementType.getTypeFromName(localName).startElement(stack, attributes);
      }
    
      @Override
      public void endElement(String uri, String localName, String qName) throws SAXException {
        if (!NAMESPACE.equals(uri)) {
          return;
        }
        ElementType.getTypeFromName(localName).endElement(stack);
      }
    Example ElementType enum
    public enum ElementType {
      *PARENT_ELEMENT*(ExampleXmlParser.*PARENT_NAME*) {
        @Override
        void startElement(Stack<Object> stack, Attributes attributes) {
          if (!(stack.peek() instanceof CacheCreation)) {
            throw new CacheXmlException("<" + *PARENT_NAME* + "> elements must occur within <cache> elements");
          }
          **create and initialize your parent Creation object and push it to the stack**
        }
    
        @Override
        void endElement(Stack<Object> stack) {
          **pop your object off the top of the stack, then add it to the CacheCreation object retrieved using stack.peek()**
        }
      },
    
      *NESTED_ELEMENT*(ExampleXmlParser.*NESTED_NAME*) {
        @Override
        void startElement(Stack<Object> stack, Attributes attributes) {
          if (!(stack.peek() instanceof *PARENT_OBJECT_CREATION*)) {
            throw new CacheXmlException("<" + *NESTED_NAME* + "> elements must occur within <" + *PARENT_NAME* + "> elements");
          }
          **create and initialize your nested object and add it to its parent Creation object** 
        }
    
        @Override
        void endElement(Stack<Object> stack) {
          // Do nothing, since the nested object has already been added to 
          // the parent object Creation
        }
      };
    
      private String typeName;
    
      ElementType(String typeName) {
        this.typeName = typeName;
      }
    
      static ElementType getTypeFromName(String typeName) {
        for (ElementType type : ElementType.values()) {
          if (type.typeName.equals(typeName))
            return type;
        }
        throw new IllegalArgumentException("Invalid type '" + typeName + "'");
      }
    
      public String getTypeName() {
        return typeName;
      }
    
      abstract void startElement(Stack<Object> stack, Attributes attributes);
    
      abstract void endElement(Stack<Object> stack);
    }
  3. Create a file named org.apache.geode.internal.cache.xmlcache.XmlParser in the resources/META-INF/services package. The file should contain the fully qualified class name of your XML parser. If a file with that name already exists, add the class name of your XML parser to it.

  4. Create an interface that extends CacheService and will serve as the interface for your service. It should have methods that take the objects created and added to the CacheCreation class in step 2 and use the information contained in them to configure the service.

  5. Implement the interface just created. Ensure that the overridden getInterface() method returns the interface created in step 4.

  6. Create a file named org.apache.geode.internal.cache.CacheService in the resources/META-INF/services package. The file should contain the fully qualified class name of the class implemented in step 5 (not the interface). If a file with that name already exists, add the class name of your cache service to it.

  7. In the CacheCreation.create() method, use the cache.getService(*YOUR_SERVICE_INTERFACE*.class) method to retrieve your service implementation and configure it using the objects created and added to CacheCreation in step 2. This completes the steps required to allow the service to be created from XML. To allow gfsh commands to update and persist the configuration of your service using the updateConfigForGroup method, the following steps are also required.

    Example service configuration during CacheCreation.create()
    public class CacheCreation implements InternalCache {
    
    ...
    
    private *YOUR_CREATION_CLASS* *YOUR_CREATION* 
    
    getter()
    setter()
    
    ...
    
      create(InternalCache cache) {
    
      ...
    
        if (*YOUR_CREATION* != null) {
          *YOUR_SERVICE* service =
              (*YOUR_SERVICE*) cache.getService(*YOUR_SERVICE_INTERFACE*.class);
          if (service != null) {
            **configure your service using the information stored in your creation**
          }
        }
    
      ...
    
      }
    }
  8. Create an annotated class in the *YOUR_PACKAGE*.management.configuration package based on your xsd schema. This can be done manually (see RegionMapping and FieldMapping) or by using the JAXB plugin for IntelliJ (while in your .xsd file, select Tools → JAXB → Generate Java Code from Xml Schema using JAXB… and then follow the instructions, making sure to specify the correct output path and package prefix).

  9. In your annotated class, add the @XSDRootElement annotation above the class definition and make the class extend CacheElement. You will need to implement the getId() method to return an appropriate identifier for this element:

    Example annotated class
    @XSDRootElement(namespace = "http://geode.apache.org/schema/*PACKAGE_NAME*",
    
        schemaLocation = "http://geode.apache.org/schema/*PACKAGE_NAME*/*SCHEMA_NAME*.xsd")
    
    public class *YOUR_ANNOTATED_CLASS* extends CacheElement {
    
    @Override
    
    public String getId() {
    
      return *IDENTIFIER*;
    
    }

    And, in the same location as your annotated class, create a package-info.java file containing the following:

    Example package-info.java
    @XmlSchema(namespace = "http://geode.apache.org/schema/*PACKAGE_NAME*",
    
        xmlns = {@XmlNs(prefix = "*ELEMENT_PREFIX*",
    
            namespaceURI = "http://geode.apache.org/schema/*PACKAGE_NAME*")},
    
        elementFormDefault = javax.xml.bind.annotation.XmlNsForm.QUALIFIED)

    where *ELEMENT_PREFIX* is the prefix to be applied to XML elements that are defined by your schema.


Final steps: make appropriate changes to excludedClasses.txt, sanctioned-geode-core-serializables.txt and assembly_content.txt to allow integration tests to pass. The easiest way to do this is to let the tests run using the following commands and follow the instructions provided by them when/if they fail:

$ ./gradlew geode-core:integrationTest --tests AnalyzeSerializablesJUnitTest

$ ./gradlew geode-assembly:integrationTest --tests AssemblyContentsIntegrationTest