Skip to content

Developer Documentation

Tobias John edited this page Oct 29, 2025 · 13 revisions

We provide instructions on how to implement four types of extensions of RDFMutate.

  1. add a new mutation operator
  2. add a new reasoner for the consistency check
  3. add a new generation strategy
  4. add a new input format for mutation operators

Add a new Mutation Operator

Implementing new mutation operators provides more freedom then expressing them using SWRL rules. This involves three steps:

  1. Implement the new mutation operator as a new class
  2. Compile the new operator to get a .class file
  3. Use the compiled mutation operator

1. Implement the new Mutation Operator

Mutation operators need to be implemented as a class and need to be a subclass of the class Mutation.

The most important attributes of the super-class are:

  • model: Model: a JenaModel that is the seed graph, which gets mutated
  • addSet: MutableSet<Statement>: the set of Statements (triples) that are added when the mutation is executed
  • removeSet: MutableSet<Statement>: the set of Statements (triples) that are removed when the mutation is executed
  • updateRequestList: List<UpdateRequest>: the list of update requests that are performed when the mutation is executed

The following methods should be implemented, i.e. overwritten:

  • isApplicable(): Boolean: a function that returns a boolean value. Return true if the mutation operator can be applied to model
  • createMutation(): the function, where the actual mutation is performed. Use model to compute, how to mutate the graph, i.e., which triples to add and to remove. You have two options: (i) You add the appropriate triples to the sets addSet and removeSet OR (ii) you add update requests to the list updateRequestList. It is not allowed to use the sets of statements and the list of update requests at the same time. Applying the changes to the seed graph is taken care of by the super-class Mutation. The function should contain a call to the function createMutation() of the super-class, which handles some required, generic setup.

2. Compile the new Mutation Operator

To compile your class, you need to add the JAR of RDFMutate to the class path to ensure that the required classes are found. You can do so with the following command if you are developing a Java class:

javac -cp rdfmutate.jar <your-class>.java

If you are developing a Kotlin class, you can use the following command:

kotlinc -cp rdfmutate.jar <your-class>.kt 

3. Use the Compiled Mutation Operator

RDFMutate does not need to be recompiled to use the new mutation operator as long as it can be found using the class path. I.e. you need to compile the source code of your operator to a .class file and need to modify the java class path accordingly. Assuming, your .class file is in a directory newOperators, you can call RDFMutate as follows to use the new operator (where <configuration-file> is a yaml configuration file that contains a reference to the new operator):

java -cp rdfmutate.jar:newOperators/* org.smolang.robust.MainKt --config=<configuration-file> 

Example

The following is a Kotlin class implementing a simple mutation operator. The operator selects a topping and adds a new node, which is a pizza with the selected topping.

import org.apache.jena.rdf.model.*
import org.apache.jena.vocabulary.RDF
import org.smolang.robust.mutant.Mutation
import kotlin.random.Random

class PizzaMutation(model: Model) : Mutation(model) {
  val topping: Resource = model.getResource(":Topping")
  val pizza: Resource = model.getResource(":Pizza")
  val hasTopping: Property = model.getProperty(":hasTopping")

  override fun createMutation() {
    val toppings = model.listResourcesWithProperty(RDF.type, topping)
    val t = toppings.toSet().random()
    val p = model.createResource(":newPizza" + Random.nextInt())
    addSet.add(model.createStatement(p, RDF.type, pizza))
    addSet.add(model.createStatement(p, hasTopping, t))
    super.createMutation()
  }
}

The following is a Kotlin class implementing the same mutation operator but making use of the updateRequestList instead of the addSet. Note, that the SAQRL query needs to include definitions for the used prefixes and how we can use LIMIT to ensure that the new pizza has only one new topping.

class PizzaMutation(model: Model) : Mutation(model) {
    override fun createMutation() {
        val newPizza = ":newPizza" + Random.nextInt()
        val query = """
                prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> 
                INSERT { 
                  $newPizza rdf:type :Pizza .
                  $newPizza :hasTopping ?t . 
                }
                WHERE { 
                  SELECT ?t  WHERE {
                    ?t rdf:type :Topping.
                  } LIMIT 1
                } 
            """.trimIndent()
        updateRequestList.add(UpdateFactory.create(query))
        super.createMutation()
    }
}

New Reasoner for Consistency Check

To add a new reasoner, you need to perform three steps.

  1. add a subclass of MaskReasoner
  2. add your reasoner to enum class ReasoningBackend
  3. add your reasoner to the MaskReasonerFactory

You need to add your reasoner as a new sub-class for the class MaskReasoner. E.g. see the Apache Jena Reasoner as an example on how to do that.

The following method needs to be implemented:

  • isConsistent(): ConsistencyResult: This function returns an object that represents if the generated model is consistent, or not. This is the main method to implement. The method itself does not take any argument, but the constructor of your reasoner probably does, e.g., the generated mutant model. The result can either be ConsistencyResult.CONSISTENT, ConsistencyResult.INCONSISTENT or ConsistencyResult.UNDECIDED, to reflect the outcome of your reasoner. The generated graph is only valid, if the result is ConsistencyResult.CONSISTENT

You also need to add your reasoner to the ReasoningBackend that is defined in the same file as the MaskReasoner here. The SerialName is used in the yaml-configuration file to select your reasoner.

The last step is to add your reasoner to the MaskReasonerFactory, analogue how it is done for all the other reasoners.

After building a new JAR file, you can use the new reasoner.

Add a custom Generation Strategy

One can add a new generation strategy that produces sequences of mutation operators. To do so, you need to perform three steps:

  1. add a subclass of MutationStrategy and implement the methods
  2. add your strategy to enum class MutationStrategyName
  3. add your strategy to the class MutationRunner

You need to add your strategy as a new sub-class of the class MutationStratgy. You can chose the attributes that you need for your strategy and you can have a look at RandomMutationStrategy as an inspiration on how to do so.

The following methods need to be implemented:

  • hasNextMutationSequence(): Boolean: returns true if the strategy can generate a new sequence of operators. If false is returned, the whole generation algorithm is terminated, even if no valid mutant graph was generated until this point.
  • getNextMutationSequence(): MutationSequence: This is the method that returns the next sequence to try. Have a look on the implementation of MutationSequence to see how to add mutation operators to an empty sequence. Most notably, one can add objects of AbstractMutation, which are extracted by the MutationRunner from the configuration and represent the selected mutation operators. Again, we refer to RandomMutationStrategy for inspiration.

You need to add your strategy to the enum class MutationStrategyName where the SerialName is the name that is used in the yaml configuration file to select the strategy.

The last step is to add your strategy to the MutationRunner class. You need to modify the method getStrategy(...), in particular, the return statement at the end. Add your strategy as an option and provide the needed arguments for your strategy. If you want to provide more arguments to your strategy than the existing strategy needs, e.g. to take the mask into account, you need to change the arguments of getStrategy(...).

After building a new JAR file, you can use the new strategy.

Custom Input Format for Mutation Operators

If our provided format using SWRL rules is not enough for your needs, you can add a custom input format for the mutation operators. You need to perform the following three steps:

  1. add a parser for your file format as a subclass of MutationFileParser
  2. add your parser to the enum class MutationOperatorFormat
  3. add your parser to MutationFileParserFactory

First, you need to implement a parser for your format. The parser should be a subclass of class MutationFileParser and implement the method getAllAbstractMutations() : List<AbstractMutation>?. Provided a file in your format, this method should produce a List<AbstractMutation> that contains the mutation operators from the file. If the file can not be parsed due to some error, e.g. because it does not exists, the result of the method is null. You can have a look at RuleParser for inspiration, which is a parser for the mutation operators specified using SWRL rules.

The class AbstractMutation can be seen as a representation of mutation operators. In the simplest case, an AbstractMutation contains only a class that represents a mutation. As your input format will (probably) allow for flexible mutation operators, the second case is more relevant: an object of AbstractMutation is represented by a Mutation class and a MutationConfiguration, where the latter contains information how the mutation is performed, i.e. some parameters. A good example is of this is RuleMutation as a subclass of Mutation to represent mutation operators from SWRL rules in general and RuleMutationConfiguration (contained in fileMutationConfiguration) that is the corresponding configuration, which contains the parameters extracted from the file with the SWRL rule. As you can see in RuleParser in the method getAllAbstractMutations(), we only extract the configurations from the file and use them to create the list of AbstractMutation objects.

Once you have your parser, you need to add it to the enum class MutationOperatorFormats, which can be found in the file MutationFileParser.kt. The SerialName is the name that can be used in the yaml configuration file to select the input format.

The last step is to add your file format in the MutationFileParserFactory, which can be found in the file MutationFileParser.kt.

After building a new JAR file, you can use the new input format.

Clone this wiki locally