public <T> T createConfiguration(Class<T> pClazz, String protocolCode, String transportCode,
                                                           String transportConfig, Map<String, List<String>> paramStringValues) {
        // Get a map of all configuration parameter fields.
        // - Get a list of all fields in the given class.
        Map<String, Field> fields = Arrays.stream(FieldUtils.getAllFields(pClazz))
            // - Filter out only the ones annotated with the ConfigurationParameter annotation.
            .filter(field -> (field.getAnnotation(ConfigurationParameter.class) != null) || (field.getAnnotation(ComplexConfigurationParameter.class) != null))
            // - Create a map with the field-name as key and the field itself as value.
            .collect(Collectors.toMap(
                ConfigurationFactory::getConfigurationName,
                Function.identity()
            ));

        // Get a list of all required configuration parameters.
        // TODO: Check for the complex-fields with required annotations.
        List<String> missingFieldNames = fields.values().stream()
            .filter(field -> field.getAnnotation(Required.class) != null)
            .map(ConfigurationFactory::getConfigurationName)
            .collect(toList());

        // Create a new instance of the configuration object.
        T instance;
        try {
            instance = pClazz.getDeclaredConstructor().newInstance();
        } catch (InvocationTargetException | InstantiationException |
                 IllegalAccessException | NoSuchMethodException e) {
            throw new IllegalArgumentException("Unable to Instantiate Configuration Class", e);
        }

        // Process the parameters passed in with the connection string.
        try {
            // Add the other codes to the param strings, so we can make them accessible from Configurations.
            // Like this:
            //    @ConfigurationParameter("protocolCode")
            //    private String protocolCode;
            paramStringValues = new HashMap<>(paramStringValues);
            List<String> previousValue;
            previousValue = paramStringValues.put("protocol-code", List.of(protocolCode));
            if (previousValue != null) {
                LOGGER.warn("protocolCode with value {} overridden by", protocolCode);
            }
            previousValue = paramStringValues.put("transport-code", List.of(transportCode));
            if (previousValue != null) {
                LOGGER.warn("transportCode with value {} overridden by", transportCode);
            }
            previousValue = paramStringValues.put("transport-config", List.of(transportConfig));
            if (previousValue != null) {
                LOGGER.warn("transportConfig with value {} overridden by", transportConfig);
            }

            // Iterate over all fields and set the values to either the values specified
            // in the param string or to defaults configured by annotations.
            for (Map.Entry<String, Field> entry : fields.entrySet()) {
                final String configName = entry.getKey();
                final Field field = fields.get(configName);
                if (field.getAnnotation(ComplexConfigurationParameter.class) != null) {
                    // Filter out only the parameters with the given prefix.
                    String prefix = field.getAnnotation(ComplexConfigurationParameter.class).prefix() + ".";
                    Map<String, List<String>> filteredParamStringValues = new HashMap<>();
                    for (String paramName : paramStringValues.keySet()) {
                        if(paramName.startsWith(prefix)) {
                            filteredParamStringValues.put(paramName.substring(prefix.length()), paramStringValues.get(paramName));
                        }
                    }
                    Class<PlcConfiguration> configType = (Class<PlcConfiguration>) field.getType();
                    PlcConfiguration configValue = createConfiguration(configType, protocolCode, transportCode, transportConfig, filteredParamStringValues);
                    FieldUtils.writeField(instance, field.getName(), configValue, true);
                } else if (paramStringValues.containsKey(configName)) {
                    String stringValue = paramStringValues.get(configName).get(0);
                    // As the arguments might be URL encoded, be sure it's decoded.
                    stringValue = URLDecoder.decode(stringValue, StandardCharsets.UTF_8);
                    FieldUtils.writeField(instance, field.getName(), toFieldValue(field, stringValue), true);
                    missingFieldNames.remove(configName);
                } else {
                    Object defaultValue = getDefaultValueFromAnnotation(field);
                    // TODO: Check if the default values type matches.
                    if (defaultValue != null) {
                        FieldUtils.writeField(instance, field.getName(), defaultValue, true);
                        missingFieldNames.remove(configName);
                    }
                }
            }

            // If in the end still some required parameters are missing, output an error.
            if (!missingFieldNames.isEmpty()) {
                throw new IllegalArgumentException("Missing required fields: " + missingFieldNames);
            }
        } catch (IllegalAccessException e) {
            throw new IllegalArgumentException("Unable to access all fields from Configuration Class '" + pClazz.getSimpleName() + "'", e);
        }
        return instance;
    }
A query contains for our cases mostly of three parts
- protocol identifier
- (optional) transport identifier
- connection address (ip/port), serial port, path (depending on the type of transport) ...
- (optional) path parameters
plc4j/spi/src/main/java/org/apache/plc4x/java/spi/configuration/ConfigurationFactory.java