code: package it.unibz.inf.ontop.rdf4j.predefined.impl;
  
  import com.google.common.collect.ImmutableList;
  import com.google.common.collect.ImmutableMap;
  import it.unibz.inf.ontop.query.RDF4JQuery;
  import it.unibz.inf.ontop.exception.MinorOntopInternalBugException;
  import it.unibz.inf.ontop.model.vocabulary.XSD;
  import it.unibz.inf.ontop.rdf4j.predefined.InvalidBindingSetException;
  import it.unibz.inf.ontop.rdf4j.predefined.PredefinedQuery;
  import it.unibz.inf.ontop.rdf4j.predefined.parsing.PredefinedQueryConfigEntry;
  import it.unibz.inf.ontop.utils.ImmutableCollectors;
  import org.eclipse.rdf4j.model.IRI;
  import org.eclipse.rdf4j.model.Value;
  import org.eclipse.rdf4j.model.ValueFactory;
  import org.eclipse.rdf4j.model.impl.SimpleValueFactory;
  import org.eclipse.rdf4j.query.BindingSet;
  import org.eclipse.rdf4j.query.impl.MapBindingSet;
  
  import java.util.Map;
  import java.util.Optional;
  import java.util.UUID;
  import java.util.concurrent.ConcurrentHashMap;
  import java.util.concurrent.ConcurrentMap;
  
  public class AbstractPredefinedQuery<Q extends RDF4JQuery> implements PredefinedQuery<Q> {
  
      private final String id;
      protected final PredefinedQueryConfigEntry queryConfig;
      private final Q inputQuery;
      private final ConcurrentMap<String, String> referenceValues;
  
      public AbstractPredefinedQuery(String id, Q inputQuery, PredefinedQueryConfigEntry queryConfig) {
          this.id = id;
          this.queryConfig = queryConfig;
          this.inputQuery = inputQuery;
          this.referenceValues = new ConcurrentHashMap<>();
      }
  
      @Override
      public Q getInputQuery() {
          return inputQuery;
      }
  
      @Override
      public String getId() {
          return id;
      }
  
      @Override
      public Optional<String> getName() {
          return queryConfig.getName();
      }
  
      @Override
      public Optional<String> getDescription() {
          return queryConfig.getDescription();
      }
  
      @Override
      public boolean shouldReturn404IfEmpty() {
          return queryConfig.shouldReturn404IfEmpty();
      }
  
      @Override
      public boolean isResultStreamingEnabled() {
          return queryConfig.isResultStreamingEnabled();
      }
  
      @Override
      public void validate(ImmutableMap<String, String> bindings) throws InvalidBindingSetException {
          ImmutableMap<String, PredefinedQueryConfigEntry.QueryParameter> parameterConfigMap = queryConfig.getParameters();
  
          ImmutableList.Builder<String> problemBuilder = ImmutableList.builder();
  
          for (Map.Entry<String, PredefinedQueryConfigEntry.QueryParameter> parameterEntry : parameterConfigMap.entrySet()) {
              String parameterId = parameterEntry.getKey();
              PredefinedQueryConfigEntry.QueryParameter queryParameter = parameterEntry.getValue();
  
              if (!bindings.containsKey(parameterId)) {
                  if (queryParameter.getRequired())
                      problemBuilder.add("The required parameter " + parameterId + " is missing");
              }
              else
                  validate(bindings.get(parameterId), parameterEntry.getValue().getType())
                          .ifPresent(problemBuilder::add);
          }
  
          ImmutableList<String> problems = problemBuilder.build();
          switch (problems.size()) {
              case 0:
                  return;
              case 1:
                  throw new InvalidBindingSetException(problems.get(0));
              default:
                  throw new InvalidBindingSetException("Invalid parameters: \n - " + String.join("\n - ", problems));
          }
      }
  
      @Override
      public BindingSet convertBindings(ImmutableMap<String, String> bindings) {
          ValueFactory valueFactory = SimpleValueFactory.getInstance();
  
          ImmutableMap<String, PredefinedQueryConfigEntry.QueryParameter> parameterConfigMap = queryConfig.getParameters();
  
          MapBindingSet bindingSet = new MapBindingSet();
  
          for (Map.Entry<String, PredefinedQueryConfigEntry.QueryParameter> parameterEntry : parameterConfigMap.entrySet()) {
              String parameterId = parameterEntry.getKey();
  
              if (bindings.containsKey(parameterId)) {
                  String lexicalValue = bindings.get(parameterId);
  
                  PredefinedQueryConfigEntry.QueryParameter queryParameter = parameterEntry.getValue();
                  bindingSet.addBinding(parameterId, convertAndValidate(lexicalValue, queryParameter.getType(), valueFactory));
              }
          }
  
          return bindingSet;
      }
  
      @Override
      public ImmutableMap<String, String> replaceWithReferenceValues(ImmutableMap<String, String> bindings) {
          ImmutableMap<String, PredefinedQueryConfigEntry.QueryParameter> parameterMap = queryConfig.getParameters();
  
          return bindings.entrySet().stream()
                  .filter(e -> parameterMap.containsKey(e.getKey()))
                  .collect(ImmutableCollectors.toMap(
                          Map.Entry::getKey,
                          e -> getReferenceValue(
                                      e.getKey(),
                                      parameterMap.get(e.getKey()))
                                  .orElseGet(e::getValue)));
      }
  
      private Value convertAndValidate(String lexicalValue, PredefinedQueryConfigEntry.QueryParameterType parameterType,
                                       ValueFactory valueFactory) {
          switch (parameterType.getCategory()) {
              case IRI:
                  return valueFactory.createIRI(lexicalValue);
              case TYPED_LITERAL:
                  return valueFactory.createLiteral(lexicalValue,
                          parameterType.getDatatypeIRI()
                                  .orElseThrow(() -> new MinorOntopInternalBugException("A typed literal must have a datatype IRI")));
              default:
                  throw new MinorOntopInternalBugException("Not supported category: " + parameterType.getCategory());
          }
      }
  
      /**
       * Returns a string for the error message if any
       *
       * TODO: implement it
       */
      private Optional<String> validate(String lexicalValue, PredefinedQueryConfigEntry.QueryParameterType parameterType) {
          return Optional.empty();
      }
  
      /**
       * Returns a reference value only if safe for random generation
       */
      private Optional<String> getReferenceValue(String paramId, PredefinedQueryConfigEntry.QueryParameter queryParameter) {
          if (!queryParameter.isSafeForRandomGeneration())
              return Optional.empty();
  
          if (!referenceValues.containsKey(paramId)) {
              synchronized (this) {
                  return Optional.of(
                          referenceValues.computeIfAbsent(
                                  paramId,
                                  k -> generateReferenceValue(queryParameter.getType())));
              }
          }
          return Optional.of(referenceValues.get(paramId));
      }
  
      private static String generateReferenceValue(PredefinedQueryConfigEntry.QueryParameterType paramType) {
  
          switch (paramType.getCategory()) {
              case IRI:
                  // TODO: support templates
                  return String.format("http://%s.example.org/fake", UUID.randomUUID().toString());
              case TYPED_LITERAL:
                  return generateReferenceLiteralValue(paramType.getDatatypeIRI()
                          .orElseThrow(() -> new MinorOntopInternalBugException("Invalid typed literal")));
              default:
                  throw new MinorOntopInternalBugException("Unexpected parameter type: " + paramType.getCategory());
          }
      }
  
      private static String generateReferenceLiteralValue(IRI datatype) {
          String iriString = datatype.stringValue();
  
          // TODO: avoid the series of IF
          if (iriString.equals(XSD.STRING.getIRIString())) {
              return UUID.randomUUID().toString();
          }
          else
              throw new RuntimeException("TODO: support random generation of " + datatype);
      }
  
  }
  