gt:             final String[] stringVariables = Arrays.stream(variables)
context: package org.betonquest.betonquest.api;
  
  import lombok.CustomLog;
  import org.betonquest.betonquest.BetonQuest;
  import org.betonquest.betonquest.GlobalObjectives;
  import org.betonquest.betonquest.Instruction;
  import org.betonquest.betonquest.config.Config;
  import org.betonquest.betonquest.database.Connector;
  import org.betonquest.betonquest.database.Saver;
  import org.betonquest.betonquest.exceptions.InstructionParseException;
  import org.betonquest.betonquest.exceptions.ObjectNotFoundException;
  import org.betonquest.betonquest.exceptions.QuestRuntimeException;
  import org.betonquest.betonquest.id.ConditionID;
  import org.betonquest.betonquest.id.EventID;
  import org.betonquest.betonquest.id.ObjectiveID;
  import org.betonquest.betonquest.utils.PlayerConverter;
  import org.bukkit.Server;
  
  import java.lang.reflect.InvocationTargetException;
  import java.util.Arrays;
  import java.util.HashMap;
  import java.util.Map;
  import java.util.Optional;
  
  /**
   * <p>
   * Superclass for all objectives. You need to extend it in order to create new
   * custom objectives.
   * </p>
   * <p>
   * Registering your objectives is done through
   * {@link org.betonquest.betonquest.BetonQuest#registerObjectives(String, Class)
   * registerObjectives()} method.
   * </p>
   */
  @SuppressWarnings({"PMD.CommentRequired", "PMD.AvoidLiteralsInIfCondition", "PMD.TooManyMethods", "PMD.GodClass"})
  @CustomLog
  public abstract class Objective {
  
      protected final int notifyInterval;
      protected final boolean notify;
      protected Instruction instruction;
      protected ConditionID[] conditions;
      protected EventID[] events;
      protected boolean persistent;
      protected boolean global;
      protected QREHandler qreHandler = new QREHandler();
  
      /**
       * Contains all data objects of the players with this objective active.
       */
      protected Map<String, ObjectiveData> dataMap = new HashMap<>();
      /**
       * Should be set with the data class used to hold players' information.
       */
      protected Class<? extends ObjectiveData> template = ObjectiveData.class;
  
      /**
       * <p>
       * Creates new instance of the objective. The objective should parse
       * instruction string at this point and extract all the data from it.
       * </p>
       * <b>Do not register listeners here!</b> There is a {@link #start()} method
       * for it.
       *
       * @param instruction Instruction object representing the objective; you need to
       *                    extract all required information from it
       * @throws InstructionParseException if the syntax is wrong or any error happens while parsing
       */
      @SuppressWarnings({"PMD.CyclomaticComplexity", "PMD.CognitiveComplexity"})
      public Objective(final Instruction instruction) throws InstructionParseException {
          this.instruction = instruction;
          // extract events and conditions
          final String[] tempEvents1 = instruction.getArray(instruction.getOptional("event"));
          final String[] tempEvents2 = instruction.getArray(instruction.getOptional("events"));
          persistent = instruction.hasArgument("persistent");
          global = instruction.hasArgument("global");
          if (global) {
              GlobalObjectives.add((ObjectiveID) instruction.getID());
          }
          // make them final
          int length = tempEvents1.length + tempEvents2.length;
          events = new EventID[length];
          for (int i = 0; i < length; i++) {
              final String event = i >= tempEvents1.length ? tempEvents2[i - tempEvents1.length] : tempEvents1[i];
              try {
                  events[i] = new EventID(instruction.getPackage(), event);
              } catch (final ObjectNotFoundException e) {
                  if (length == 1 && "ID is null".equals(e.getMessage())) {
                      throw new InstructionParseException("Error while parsing objective events: No events are defined!", e);
                  }
                  throw new InstructionParseException("Error while parsing objective events: " + e.getMessage(), e);
              }
          }
          final String[] tempConditions1 = instruction.getArray(instruction.getOptional("condition"));
          final String[] tempConditions2 = instruction.getArray(instruction.getOptional("conditions"));
          length = tempConditions1.length + tempConditions2.length;
          conditions = new ConditionID[length];
          for (int i = 0; i < length; i++) {
              final String condition = i >= tempConditions1.length ? tempConditions2[i - tempConditions1.length]
                      : tempConditions1[i];
              try {
                  conditions[i] = new ConditionID(instruction.getPackage(), condition);
              } catch (final ObjectNotFoundException e) {
                  throw new InstructionParseException("Error while parsing objective conditions: " + e.getMessage(), e);
              }
          }
          final int customNotifyInterval = instruction.getInt(instruction.getOptional("notify"), 0);
          notify = customNotifyInterval > 0 || instruction.hasArgument("notify");
          notifyInterval = Math.max(1, customNotifyInterval);
      }
  
      /**
       * This method is called by the plugin when the objective needs to start
       * listening for events. Register your Listeners here!
       */
      public abstract void start();
  
      /**
       * This method is called by the plugin when the objective starts for a specific player.
       */
      @SuppressWarnings("PMD.EmptyMethodInAbstractClassShouldBeAbstract")
      public void start(final String playerId) {
          //Empty
      }
  
      /**
       * This method is called by the plugin when the objective needs to be
       * stopped. You have to unregister all Listeners here.
       */
      public abstract void stop();
  
      /**
       * This method is called by the plugin when the objective stop for a specific player.
       */
      @SuppressWarnings("PMD.EmptyMethodInAbstractClassShouldBeAbstract")
      public void stop(final String playerId) {
          //Empty
      }
  
      /**
       * This method should return the default data instruction for the objective,
       * ready to be parsed by the ObjectiveData class.
       *
       * @return the default data instruction string
       */
      public abstract String getDefaultDataInstruction();
  
      /**
       * This method should return the default data instruction for the objective,
       * ready to be parsed by the ObjectiveData class.
       * Reimplement this method if you need player context (e.g. for variable parsing) when creating the data instruction.
       *
       * @param playerID player to parse the instruction for
       * @return the default data instruction string
       */
      public String getDefaultDataInstruction(final String playerID) {
          return getDefaultDataInstruction();
      }
  
      /**
       * This method should return various properties of the objective, formatted
       * as readable Strings. An example would be "5h 5min" for "time_left"
       * keyword in "delay" objective or "12" for keyword "mobs_killed" in
       * "mobkill" objective. The method is not abstract since not all objectives
       * need to have properties, i.e. "die" objective. By default it returns an
       * empty string.
       *
       * @param name     the name of the property you need to return; you can parse it
       *                 to extract additional information
       * @param playerID ID of the player for whom the property is to be returned
       * @return the property with given name
       */
      abstract public String getProperty(final String name, final String playerID);
  
      /**
       * This method fires events for the objective and removes it from player's
       * list of active objectives. Use it when you detect that the objective has
       * been completed. It deletes the objective using delete() method.
       *
       * @param playerID the ID of the player for whom the objective is to be completed
       */
      public final void completeObjective(final String playerID) {
          // remove the objective from player's list
          completeObjectiveForPlayer(playerID);
          BetonQuest.getInstance().getPlayerData(playerID).removeRawObjective((ObjectiveID) instruction.getID());
          if (persistent) {
              BetonQuest.getInstance().getPlayerData(playerID).addNewRawObjective((ObjectiveID) instruction.getID());
              createObjectiveForPlayer(playerID, getDefaultDataInstruction(playerID));
          }
          LOG.debug(instruction.getPackage(),
                  "Objective \"" + instruction.getID().getFullID() + "\" has been completed for player "
                          + PlayerConverter.getName(playerID)
                          + ", firing events.");
          // fire all events
          for (final EventID event : events) {
              BetonQuest.event(playerID, event);
          }
          LOG.debug(instruction.getPackage(),
                  "Firing events in objective \"" + instruction.getID().getFullID() + "\" for player "
                          + PlayerConverter.getName(playerID)
                          + " finished");
      }
  
      /**
       * Checks if all conditions has been met. Use it when the player has done
       * something that modifies data (e.g. killing zombies). If conditions are
       * met, you can safely modify the data.
       *
       * @param playerID ID of the player for whom the conditions are to be checked
       * @return if all conditions of this objective has been met
       */
      public final boolean checkConditions(final String playerID) {
          LOG.debug(instruction.getPackage(), "Condition check in \"" + instruction.getID().getFullID()
                  + "\" objective for player " + PlayerConverter.getName(playerID));
          return BetonQuest.conditions(playerID, conditions);
      }
  
      /**
       * Send notification for progress with the objective.
       *
       * @param messageName meessage name to use in messages.yml
       * @param playerID    player to send notification to
       * @param variables   variables for putting into the message
       */
      protected void sendNotify(final String playerID, final String messageName, final Object... variables) {
          try {
  