/**
   * Set the objects properties to the values in this setter. Iterates through each of the
   * properties in this setter, searches for a matching setter method for the property, and if
   * found, calls the setter with this object's property value. Can also insert values from the
   * attributes themselves, by using the %attributeName% placeholder in the value string.
   *
   * @param context Layer's context url
   * @param attributeValues Attribute values
   * @param objects Objects to search for matching setter properties, using reflection
   */
  public void setPropertiesFromAttributes(URL context, AVList attributeValues, Object... objects) {
    Map<String, Method> methods = new HashMap<String, Method>();
    Map<Method, Object> methodToObject = new HashMap<Method, Object>();

    // create a list of the methods in the objects
    for (Object object : objects) {
      for (Method method : object.getClass().getMethods()) {
        methods.put(method.getName(), method);
        methodToObject.put(method, object);
      }
    }

    // for each of the properties in this setter
    for (Entry<String, String> entry : properties.entrySet()) {
      // search for the setter method for this property
      String property = entry.getKey();
      String methodName = constructSetterName(property);
      if (!methods.containsKey(methodName)) {
        String message = "Could not find setter method '" + methodName + "' in class: ";
        for (Object object : objects) {
          message += object.getClass() + ", ";
        }
        message = message.substring(0, message.length() - 2);

        Logging.logger().warning(message);
        continue;
      }

      // find out the method's parameters
      Method setter = methods.get(methodName);
      Object object = methodToObject.get(setter);
      Class<?>[] parameters = setter.getParameterTypes();

      // get the string value to pass to the method
      String stringValue = entry.getValue();
      stringValue = replaceVariablesWithAttributeValues(stringValue, attributeValues);

      String[] paramValueStrings = splitPipeSeparatedString(stringValue);

      if (parameters.length != paramValueStrings.length) {
        String message =
            "Setter method '"
                + methodName
                + "' in class "
                + object.getClass()
                + " doesn't take "
                + paramValueStrings.length
                + " parameter(s)";
        Logging.logger().severe(message);
        // Continue on incase this is an overloaded method
        continue;
      }

      Object[] parameterValues = new Object[paramValueStrings.length];
      String[] typeOverrides = getTypeOverridesForProperty(property, parameterValues.length);

      // Convert each parameter value string into a parameter
      for (int i = 0; i < paramValueStrings.length; i++) {
        // find out the type to pass to the method
        Class<?> parameterType = parameters[i];
        Class<?> type = parameterType;

        // check if the type has been overridden (useful if the type above is just 'Object')
        String typeOverride = typeOverrides[i];
        if (!isBlank(typeOverride)) {
          type = convertTypeToClass(typeOverride);
          if (type == null) {
            String message = "Could not find class for type " + type;
            Logging.logger().severe(message);
            throw new IllegalArgumentException(message);
          } else if (!parameterType.isAssignableFrom(type)) {
            String message =
                "Setter method '"
                    + methodName
                    + "' in class "
                    + object.getClass()
                    + " parameter type "
                    + parameterType
                    + " not assignable from type "
                    + type;
            Logging.logger().severe(message);
            throw new IllegalArgumentException(message);
          }
        }

        // convert the string value to a valid type
        Object value = convertStringToType(context, paramValueStrings[i], type);
        if (value == null) {
          String message = "Error converting '" + paramValueStrings[i] + "' to type " + type;
          Logging.logger().severe(message);
          throw new IllegalArgumentException(message);
        }

        parameterValues[i] = value;
      }

      // invoke the setter with the value
      try {
        setter.invoke(object, parameterValues);
      } catch (Exception e) {
        String message =
            "Error invoking '" + methodName + "' in class " + object.getClass() + ": " + e;
        Logging.logger().severe(message);
        throw new IllegalArgumentException(message, e);
      }
    }
  }
  /**
   * Convert a string to a certain type, parsing the string if required
   *
   * @param context If creating a URL, use this as the URL's context
   * @param string String to convert
   * @param type Type to convert to
   * @return Converted string, or null if failed
   */
  protected static Object convertStringToType(URL context, String string, Class<?> type) {
    try {
      if (type.isAssignableFrom(String.class)) {
        return string;
      } else if (type.isAssignableFrom(Double.class) || type.isAssignableFrom(double.class)) {
        return Double.valueOf(string);
      } else if (type.isAssignableFrom(Integer.class) || type.isAssignableFrom(int.class)) {
        return Integer.decode(string);
      } else if (type.isAssignableFrom(Float.class) || type.isAssignableFrom(float.class)) {
        return Float.valueOf(string);
      } else if (type.isAssignableFrom(Long.class) || type.isAssignableFrom(long.class)) {
        return Long.decode(string);
      } else if (type.isAssignableFrom(Character.class) || type.isAssignableFrom(char.class)) {
        return string.charAt(0);
      } else if (type.isAssignableFrom(Byte.class) || type.isAssignableFrom(byte.class)) {
        return Byte.decode(string);
      } else if (type.isAssignableFrom(Boolean.class) || type.isAssignableFrom(boolean.class)) {
        return Boolean.valueOf(string);
      } else if (type.isAssignableFrom(URL.class)) {
        try {
          return new URL(context, string);
        } catch (MalformedURLException e) {
        }
      } else if (type.isAssignableFrom(File.class)) {
        return new File(string);
      } else if (type.isAssignableFrom(Color.class)) {
        int[] ints = splitInts(string);
        if (ints.length == 1) return new Color(ints[0]);
        else if (ints.length == 3) return new Color(ints[0], ints[1], ints[2]);
        else if (ints.length == 4) return new Color(ints[0], ints[1], ints[2], ints[3]);
      } else if (type.isAssignableFrom(Dimension.class)) {
        int[] ints = splitInts(string);
        if (ints.length == 1) return new Dimension(ints[0], ints[0]);
        else if (ints.length == 2) return new Dimension(ints[0], ints[1]);
      } else if (type.isAssignableFrom(Point.class)) {
        int[] ints = splitInts(string);
        if (ints.length == 1) return new Point(ints[0], ints[0]);
        else if (ints.length == 2) return new Point(ints[0], ints[1]);
      } else if (type.isAssignableFrom(Font.class)) {
        return Font.decode(string);
      } else if (type.isAssignableFrom(Material.class)) {
        Color color = null;
        int[] ints = splitInts(string);
        if (ints.length == 1) color = new Color(ints[0]);
        else if (ints.length == 3) color = new Color(ints[0], ints[1], ints[2]);
        else if (ints.length == 4) color = new Color(ints[0], ints[1], ints[2], ints[3]);

        if (color != null) return new Material(color);
      } else if (type.isAssignableFrom(Insets.class)) {
        int[] ints = splitInts(string);
        if (ints.length == 4) return new Insets(ints[0], ints[1], ints[2], ints[3]);
      }
    } catch (Exception e) {

    }
    return null;
  }