Reworked setting validation API-s, and some other cleanup of old code.
diff --git a/freemarker-docgen-core/pom.xml b/freemarker-docgen-core/pom.xml
index 84bd7e7..952fccc 100644
--- a/freemarker-docgen-core/pom.xml
+++ b/freemarker-docgen-core/pom.xml
@@ -112,6 +112,23 @@
             <artifactId>commons-text</artifactId>
             <version>1.9</version>
         </dependency>
+        <dependency>
+            <groupId>com.google.guava</groupId>
+            <artifactId>guava</artifactId>
+            <version>30.1-jre</version>
+        </dependency>
+        <dependency>
+            <groupId>org.junit.jupiter</groupId>
+            <artifactId>junit-jupiter-engine</artifactId>
+            <version>5.7.0</version>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.hamcrest</groupId>
+            <artifactId>hamcrest</artifactId>
+            <version>2.2</version>
+            <scope>test</scope>
+        </dependency>
     </dependencies>
 
     <build>
diff --git a/freemarker-docgen-core/src/main/java/org/freemarker/docgen/core/CJSONInterpreter.java b/freemarker-docgen-core/src/main/java/org/freemarker/docgen/core/CJSONInterpreter.java
index 97aca12..543aea6 100644
--- a/freemarker-docgen-core/src/main/java/org/freemarker/docgen/core/CJSONInterpreter.java
+++ b/freemarker-docgen-core/src/main/java/org/freemarker/docgen/core/CJSONInterpreter.java
@@ -483,27 +483,33 @@
     /**
      * Returns the type-name of a value according to the CJSON language.
      */
-    public static String cjsonTypeOf(Object value) {
-        if (value instanceof String) {
+    public static String cjsonTypeNameOfValue(Object value) {
+        return cjsonTypeNameForClass(value != null ? value.getClass() : null);
+    }
+
+    public static String cjsonTypeNameForClass(Class<?> cl) {
+        if (String.class.isAssignableFrom(cl)) {
             return "string";
-        } else if (value instanceof Number) {
-            return "number";
-        } else if (value instanceof Boolean) {
+        } else if (Integer.class.isAssignableFrom(cl)) {
+            return "int";
+        } else if (Long.class.isAssignableFrom(cl)) {
+            return "long";
+        } else if (Double.class.isAssignableFrom(cl)) {
+            return "double";
+        } else if (BigDecimal.class.isAssignableFrom(cl)) {
+            return "big-decimal";
+        } else if (Boolean.class.isAssignableFrom(cl)) {
             return "boolean";
-        } else if (value instanceof List<?>) {
+        } else if (List.class.isAssignableFrom(cl)) {
             return "list";
-        } else if (value instanceof LinkedHashMap<?, ?>) {
+        } else if (LinkedHashMap.class.isAssignableFrom(cl)) {
+            return "map (order keeping)";
+        } else if (Map.class.isAssignableFrom(cl)) {
             return "map";
-        } else if (value instanceof Map<?, ?>) {
-            return "map (unordered)";
-        } else if (value instanceof FunctionCall) {
+        } else if (FunctionCall.class.isAssignableFrom(cl)) {
             return "function call";
         } else {
-            if (value != null) {
-                return value.getClass().getName();
-            } else {
-                return "null";
-            }
+            return cl != null ? cl.getName() : "null";
         }
     }
 
@@ -643,7 +649,7 @@
                     if (keyFunc != o1) {
                         throw newError(
                                 "The key must be a String, but it is a(n) "
-                                + cjsonTypeOf(o1) + ".", keyP);
+                                + cjsonTypeNameOfValue(o1) + ".", keyP);
                     } else {
                         throw newError(
                                 "You can't use the function here, "
@@ -733,7 +739,7 @@
                         throw newError(
                                 "This expression should be either a string "
                                 + "or a map, but it is a(n) "
-                                + cjsonTypeOf(o1) + ".", keyP);
+                                + cjsonTypeNameOfValue(o1) + ".", keyP);
                     }
                 } else {
                     if (o1 instanceof Map) {
@@ -748,7 +754,7 @@
                         } else {
                             throw newError(
                                     "Function doesn't evalute to a map, but "
-                                    + "to " + cjsonTypeOf(o1)
+                                    + "to " + cjsonTypeNameOfValue(o1)
                                     + ", so it can't be merged into the map.",
                                     keyP);
                         }
diff --git a/freemarker-docgen-core/src/main/java/org/freemarker/docgen/core/DocgenException.java b/freemarker-docgen-core/src/main/java/org/freemarker/docgen/core/DocgenException.java
index 3ec8229..e3521fd 100644
--- a/freemarker-docgen-core/src/main/java/org/freemarker/docgen/core/DocgenException.java
+++ b/freemarker-docgen-core/src/main/java/org/freemarker/docgen/core/DocgenException.java
@@ -21,7 +21,7 @@
 /**
  * Exception that is docgen-specific. 
  */
-public class DocgenException extends Exception {
+public class DocgenException extends RuntimeException {
     
     public DocgenException(String message) {
         super(message);
diff --git a/freemarker-docgen-core/src/main/java/org/freemarker/docgen/core/DocgenTagException.java b/freemarker-docgen-core/src/main/java/org/freemarker/docgen/core/DocgenTagException.java
new file mode 100644
index 0000000..6d7ed81
--- /dev/null
+++ b/freemarker-docgen-core/src/main/java/org/freemarker/docgen/core/DocgenTagException.java
@@ -0,0 +1,37 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.freemarker.docgen.core;
+
+import freemarker.core.Environment;
+import freemarker.template.TemplateException;
+
+/**
+ * Exception thrown by docgen tag-s that are inside the XML text. As such, it's treated as the mistake of the document
+ * author (as opposed to an internal error).
+ */
+public class DocgenTagException extends TemplateException {
+    public DocgenTagException(String description, Environment env) {
+        super(description, env);
+    }
+
+    public DocgenTagException(String description, Throwable cause, Environment env) {
+        super(description, cause, env);
+    }
+}
diff --git a/freemarker-docgen-core/src/main/java/org/freemarker/docgen/core/Logo.java b/freemarker-docgen-core/src/main/java/org/freemarker/docgen/core/Logo.java
index 554317c..4abfd3d 100644
--- a/freemarker-docgen-core/src/main/java/org/freemarker/docgen/core/Logo.java
+++ b/freemarker-docgen-core/src/main/java/org/freemarker/docgen/core/Logo.java
@@ -22,32 +22,30 @@
 /** Model for a logo shown */
 public class Logo {
 
-    private String src;
+    private final String src;
     private String href;
-    private String alt;
-    
+    private final String alt;
+
+    public Logo(String src, String href, String alt) {
+        this.src = src;
+        this.href = href;
+        this.alt = alt;
+    }
+
     public String getSrc() {
         return src;
     }
     
-    public void setSrc(String src) {
-        this.src = src;
-    }
-    
     public String getHref() {
         return href;
     }
-    
+
     public void setHref(String href) {
         this.href = href;
     }
-    
+
     public String getAlt() {
         return alt;
     }
-    
-    public void setAlt(String alt) {
-        this.alt = alt;
-    }
-    
+
 }
diff --git a/freemarker-docgen-core/src/main/java/org/freemarker/docgen/core/SettingName.java b/freemarker-docgen-core/src/main/java/org/freemarker/docgen/core/SettingName.java
new file mode 100644
index 0000000..78bb341
--- /dev/null
+++ b/freemarker-docgen-core/src/main/java/org/freemarker/docgen/core/SettingName.java
@@ -0,0 +1,82 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.freemarker.docgen.core;
+
+import java.io.File;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Optional;
+
+final class SettingName {
+    private final File parentFile;
+    private final SettingName parent;
+    private final Object key;
+
+    public SettingName(File parentFile, SettingName parent, Object key) {
+        this.parentFile = parentFile;
+        this.parent = parent;
+        this.key = key;
+    }
+
+    static SettingName topLevel(File parentFile, String simpleName) {
+        return new SettingName(parentFile, null, simpleName);
+    }
+
+    SettingName subKey(Object key) {
+        return new SettingName(null, this, key);
+    }
+
+    SettingName subKey(Object... keys) {
+        return new SettingName(null,this, subKey(Arrays.asList(keys)));
+    }
+
+    SettingName subKey(List<Object> keys) {
+        SettingName result = this;
+        for (Object key : keys) {
+            result = new SettingName(null, result, key);
+        }
+        return result;
+    }
+
+    Optional<File> getContainingFile() {
+        return parent != null ? parent.getContainingFile() : Optional.ofNullable(parentFile);
+    }
+
+    @Override
+    public String toString() {
+        StringBuilder sb = new StringBuilder();
+        appendName(sb);
+        return sb.toString();
+    }
+
+    private void appendName(StringBuilder sb) {
+        if (parent != null) {
+            parent.appendName(sb);
+        }
+        if (key instanceof String) {
+            if (sb.length() != 0) {
+                sb.append('.');
+            }
+            sb.append(key);
+        } else {
+            sb.append('[').append(key).append(']');
+        }
+    }
+}
diff --git a/freemarker-docgen-core/src/main/java/org/freemarker/docgen/core/SettingUtils.java b/freemarker-docgen-core/src/main/java/org/freemarker/docgen/core/SettingUtils.java
new file mode 100644
index 0000000..c53d905
--- /dev/null
+++ b/freemarker-docgen-core/src/main/java/org/freemarker/docgen/core/SettingUtils.java
@@ -0,0 +1,340 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.freemarker.docgen.core;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Sets;
+
+import freemarker.template.utility.StringUtil;
+
+final class SettingUtils {
+    private SettingUtils() {
+        throw new AssertionError();
+    }
+
+    static DocgenException newCfgFileException(SettingName settingName, String desc) {
+        return newCfgFileException(settingName, desc, null);
+    }
+
+    static DocgenException newCfgFileException(SettingName settingName, String desc, Throwable cause) {
+        StringBuilder sb = new StringBuilder();
+        sb.append("Wrong configuration");
+        if (settingName != null) {
+            sb.append(" setting \"").append(settingName).append("\"");
+        }
+        settingName.getContainingFile().ifPresent(containingFile -> sb.append(" in file \"").append(containingFile.getAbsolutePath()).append("\""));
+        sb.append(":\n");
+        sb.append(desc);
+        return new DocgenException(sb.toString(), cause);
+    }
+
+    @SuppressWarnings("unchecked")
+    static <K, V> Map<K, V> castSettingToMap(
+            SettingName settingName, Object settingValue,
+            Class<K> keyClass, Class<V> valueClass) {
+        return castSettingToMap(settingName, settingValue, keyClass, valueClass, false);
+    }
+
+    @SuppressWarnings("unchecked")
+    static <K, V> Map<K, V> castSettingToMap(
+            SettingName settingName, Object settingValue,
+            Class<K> keyClass, Class<V> valueClass, boolean allowNullValueInMap) {
+        return (Map<K, V>) castSetting(
+                settingName, settingValue,
+                Map.class,
+                new MapEntryType(keyClass, valueClass, allowNullValueInMap));
+    }
+
+    @SuppressWarnings("unchecked")
+    static <T> List<T> castSettingToList(
+            SettingName settingName,
+            Object settingValue, Class<T> elementClass) {
+        return castSetting(
+                settingName, settingValue,
+                false,
+                List.class, new ListItemType(elementClass)
+        );
+    }
+
+    static <T> T castSetting(SettingName settingName, Object settingValue, Class<T> valueType) {
+        return castSetting(settingName, settingValue, false, valueType);
+    }
+
+    /**
+     * Same as {@link #castSetting(List, Object, boolean, Class, List)} with {@code optional} {@code false}.
+     */
+    static <T> T castSetting(
+            SettingName settingName, Object settingValue, Class<T> valueType,
+            ContainedValueType... containedValueTypes) {
+        return castSetting(settingName, settingValue, false, valueType, containedValueTypes);
+    }
+
+    /**
+     * @param valueType
+     *      The expected type of the value (on the top-level, if it's a container)
+     * @param containedValueTypes
+     *      The expected type of the contained values, and of the values contained inside them, and so on. (This is
+     *      separate from {@code valueType} because Java can't match s generic return type with the type of the first
+     */
+    static <T> T castSetting(
+            SettingName settingName, Object settingValue,
+            boolean optional,
+            Class<T> valueType, ContainedValueType... containedValueTypes) {
+        if (settingValue == null) {
+            if (optional) {
+                return null;
+            }
+            throw newNullSettingValueException(settingName);
+        }
+        if (!valueType.isInstance(settingValue)) {
+            System.out.println("BAD VALUE: " + settingValue); //!!T
+            throw newBadSettingValueTypeException(settingName, valueType, settingValue);
+        }
+
+        checkContainedValueTypes(settingName, settingValue, containedValueTypes);
+
+        return (T) settingValue;
+    }
+
+    static void checkContainedValueTypes(
+            SettingName settingName, Object settingValue,
+            ContainedValueType... containedValueTypes)  {
+        if (containedValueTypes.length == 0) {
+            return;
+        }
+        checkContainedValueTypes(settingName, settingValue, new ArrayList<>(containedValueTypes.length),
+                containedValueTypes);
+    }
+
+    private static void checkContainedValueTypes(
+            SettingName settingName, Object containerValue,
+            List<Object> checkedContainedSettingNameTail, ContainedValueType... containedValueTypes) {
+        if (checkedContainedSettingNameTail.size() == containedValueTypes.length || containerValue == null) {
+            return;
+        }
+
+        Class<? extends Object> containerClass = containerValue.getClass();
+        ContainedValueType containedValueType = containedValueTypes[checkedContainedSettingNameTail.size()];
+        checkContainerClassIsValidContainedValueType(containerClass, containedValueType);
+        if (containedValueType instanceof ListItemType) {
+            int listElementIndex = 0;
+            for (Object listElement : ((List<?>) containerValue)) {
+                if (listElement == null) {
+                    if (!containedValueType.allowNullValue) {
+                        throw newNullSettingValueException(
+                                settingName.subKey(checkedContainedSettingNameTail).subKey(listElementIndex));
+                    }
+                } else if (!containedValueType.valueType.isInstance(listElement)) {
+                    throw newBadSettingValueTypeException(
+                            settingName.subKey(checkedContainedSettingNameTail).subKey(listElementIndex),
+                            containedValueType.valueType, listElement);
+                }
+
+                checkedContainedSettingNameTail.add(listElementIndex);
+                try {
+                    checkContainedValueTypes(
+                            settingName, listElement, checkedContainedSettingNameTail,
+                            containedValueTypes);
+                } finally {
+                    checkedContainedSettingNameTail.remove(checkedContainedSettingNameTail.size() - 1);
+                }
+                listElementIndex++;
+            }
+        } else if (containedValueType instanceof MapEntryType) {
+            MapEntryType mapEntryType = (MapEntryType) containedValueType;
+            for (Map.Entry<?, ?> mapEntry : ((Map<?, ?>) containerValue).entrySet()) {
+                Object entryKey = mapEntry.getKey();
+                if (entryKey == null) {
+                    throw newCfgFileException(
+                            settingName, "Null keys aren't allowed in this setting value.");
+                }
+                Class<?> keyType = mapEntryType.keyType;
+                if (!keyType.isInstance(entryKey)) {
+                    throw newCfgFileException(
+                            settingName.subKey(checkedContainedSettingNameTail), // Don't add the key.
+                            "Expected key type " + CJSONInterpreter.cjsonTypeNameForClass(keyType)
+                                    + ", but key was of type " + CJSONInterpreter.cjsonTypeNameOfValue(entryKey));
+                }
+
+                Object entryValue = mapEntry.getValue();
+                if (entryValue == null) {
+                    if (!containedValueType.allowNullValue) {
+                        throw newNullSettingValueException(
+                                settingName.subKey(checkedContainedSettingNameTail).subKey(entryKey));
+                    }
+                } else if (!containedValueType.valueType.isInstance(entryValue)) {
+                    throw newBadSettingValueTypeException(
+                            settingName.subKey(checkedContainedSettingNameTail).subKey(entryKey),
+                            containedValueType.valueType, entryValue);
+                }
+
+                checkedContainedSettingNameTail.add(entryKey);
+                try {
+                    checkContainedValueTypes(
+                            settingName, entryValue, checkedContainedSettingNameTail,
+                            containedValueTypes);
+                } finally {
+                    checkedContainedSettingNameTail.remove(checkedContainedSettingNameTail.size() - 1);
+                }
+            }
+            if (mapEntryType.validateKeys) {
+                checkMapKeys(settingName, (Map) containerValue, mapEntryType.requiredKeys, mapEntryType.optionalKeys);
+            }
+        } else {
+            throw new AssertionError();
+        }
+    }
+
+    private static void checkContainerClassIsValidContainedValueType(
+            Class<?> containerClass, ContainedValueType containedValueType) {
+        if (!containedValueType.isValidContainerClass(containerClass)) {
+            throw new IllegalArgumentException(
+                    containedValueType.getClass().getSimpleName()
+                            + " is not fitting for provided container value class, "
+                            + containerClass.getSimpleName() + ".");
+        }
+    }
+
+    private static DocgenException newBadSettingValueTypeException(SettingName settingName, Class<?> expectedValueType,
+            Object settingValue) throws
+            DocgenException {
+        return newCfgFileException(
+                settingName,
+                "Setting value should be a(n) " + CJSONInterpreter.cjsonTypeNameForClass(expectedValueType) + ", "
+                        + "but was a(n) " + CJSONInterpreter.cjsonTypeNameOfValue(settingValue) + ".");
+    }
+
+    private static DocgenException newNullSettingValueException(SettingName settingName) {
+        return newCfgFileException(
+                settingName,
+                "Setting is required but wasn't set (or was set to null).");
+    }
+
+    private static <T> void checkMapKeys(
+            SettingName settingName, Map<T, ?> value,
+            Set<T> requiredKeys, Set<T> optionalKeys) {
+        Set<T> mapKeySet = value.keySet();
+        for (T key : mapKeySet) {
+            if (!requiredKeys.contains(key) && !optionalKeys.contains(key)) {
+                throw newCfgFileException(settingName,
+                        "Unsupported key in the map value: " + StringUtil.jQuote(key) + ". Valid keys are: "
+                                + Sets.union(requiredKeys, optionalKeys).stream()
+                                .sorted()
+                                .map(it -> StringUtil.jQuote(it))
+                                .collect(Collectors.joining(", ")));
+            }
+        }
+        for (T requiredKey : requiredKeys) {
+            if (!mapKeySet.contains(requiredKey)) {
+                throw newCfgFileException(settingName, "Required key is missing from the map value: " + requiredKey);
+            }
+        }
+    }
+
+    abstract static class ContainedValueType {
+        private final Class<?> valueType;
+        private final boolean allowNullValue;
+
+        private ContainedValueType(Class<?> valueType, boolean allowNullValue) {
+            this.valueType = Objects.requireNonNull(valueType);
+            this.allowNullValue = allowNullValue;
+        }
+
+        public abstract boolean isValidContainerClass(Class<?> containerClass);
+    }
+
+    final static class ListItemType extends ContainedValueType {
+        public ListItemType(Class<?> valueType) {
+            this(valueType, false);
+        }
+
+        public ListItemType(Class<?> valueType, boolean allowNullValue) {
+            super(valueType, allowNullValue);
+        }
+
+        @Override
+        public boolean isValidContainerClass(Class<?> containerClass) {
+            return List.class.isAssignableFrom(containerClass);
+        }
+    }
+
+    final static class MapEntryType<T> extends ContainedValueType {
+        private final Class<T> keyType;
+        private final boolean validateKeys;
+        private final Set<T> requiredKeys;
+        private final Set<T> optionalKeys;
+
+        public MapEntryType(Class<T> keyType, Class<?> valueType) {
+            this(keyType, valueType, false);
+        }
+
+        public MapEntryType(Class<T> keyType, Class<?> valueType, boolean allowNullValue) {
+            this(keyType, false, Collections.emptySet(), Collections.emptySet(), valueType, allowNullValue);
+        }
+
+        public MapEntryType(
+                Class<T> keyType, Set<T> requiredKeys,
+                Class<?> valueType) {
+            this(keyType, true, requiredKeys, Collections.emptySet(), valueType, false);
+        }
+
+        public MapEntryType(
+                Class<T> keyType, Set<T> requiredKeys,
+                Class<?> valueType, boolean allowNullValue) {
+            this(keyType, true, requiredKeys, Collections.emptySet(), valueType, allowNullValue);
+        }
+
+        public MapEntryType(
+                Class<T> keyType, Set<T> requiredKeys, Set<T> optionalKeys,
+                Class<?> valueType) {
+            this(keyType, true, requiredKeys, optionalKeys, valueType, false);
+        }
+
+        public MapEntryType(
+                Class<T> keyType, Set<T> requiredKeys, Set<T> optionalKeys,
+                Class<?> valueType, boolean allowNullValue) {
+            this(keyType, true, requiredKeys, optionalKeys, valueType, allowNullValue);
+        }
+
+        private MapEntryType(
+                Class<T> keyType, boolean validateKeys, Set<T> requiredKeys, Set<T> optionalKeys,
+                Class<?> valueType, boolean allowNullValue) {
+            super(valueType, allowNullValue);
+            this.keyType = Objects.requireNonNull(keyType);
+            this.validateKeys = validateKeys;
+            this.requiredKeys = Objects.requireNonNull(requiredKeys);
+            this.optionalKeys = Objects.requireNonNull(optionalKeys);
+        }
+
+        @Override
+        public boolean isValidContainerClass(Class<?> containerClass) {
+            return Map.class.isAssignableFrom(containerClass);
+        }
+    }
+
+}
diff --git a/freemarker-docgen-core/src/main/java/org/freemarker/docgen/core/Transform.java b/freemarker-docgen-core/src/main/java/org/freemarker/docgen/core/Transform.java
index 204e492..37da9ab 100644
--- a/freemarker-docgen-core/src/main/java/org/freemarker/docgen/core/Transform.java
+++ b/freemarker-docgen-core/src/main/java/org/freemarker/docgen/core/Transform.java
@@ -19,6 +19,7 @@
 package org.freemarker.docgen.core;
 
 import static org.freemarker.docgen.core.DocBook5Constants.*;
+import static org.freemarker.docgen.core.SettingUtils.*;
 
 import java.io.BufferedReader;
 import java.io.File;
@@ -30,6 +31,7 @@
 import java.nio.charset.StandardCharsets;
 import java.nio.file.Files;
 import java.nio.file.Path;
+import java.nio.file.Paths;
 import java.text.Collator;
 import java.util.ArrayList;
 import java.util.Collections;
@@ -43,11 +45,13 @@
 import java.util.Locale;
 import java.util.Map;
 import java.util.Map.Entry;
+import java.util.Objects;
 import java.util.Set;
 import java.util.SortedMap;
 import java.util.TimeZone;
 import java.util.TreeMap;
 import java.util.regex.Pattern;
+import java.util.stream.Collectors;
 
 import org.w3c.dom.Document;
 import org.w3c.dom.Element;
@@ -55,6 +59,11 @@
 import org.w3c.dom.NodeList;
 import org.xml.sax.SAXException;
 
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Sets;
+
 import freemarker.cache.ClassTemplateLoader;
 import freemarker.cache.FileTemplateLoader;
 import freemarker.cache.MultiTemplateLoader;
@@ -117,6 +126,13 @@
     static final String SETTING_LOGO_KEY_SRC = "src";
     static final String SETTING_LOGO_KEY_ALT = "alt";
     static final String SETTING_LOGO_KEY_HREF = "href";
+    static final Set<String> SETTING_LOGO_MAP_KEYS;
+    static {
+        SETTING_LOGO_MAP_KEYS = new LinkedHashSet<>();
+        SETTING_LOGO_MAP_KEYS.add(SETTING_LOGO_KEY_SRC);
+        SETTING_LOGO_MAP_KEYS.add(SETTING_LOGO_KEY_ALT);
+        SETTING_LOGO_MAP_KEYS.add(SETTING_LOGO_KEY_HREF);
+    }
     static final String SETTING_SIDE_TOC_LOGOS = "sideTOCLogos";
     static final String SETTING_TABS = "tabs";
     static final String SETTING_SECONDARY_TABS = "secondaryTabs";
@@ -316,7 +332,7 @@
     /** Elements for which an id attribute automatically added if missing */
     private static final Set<String> GUARANTEED_ID_ELEMENTS;
     static {
-        Set<String> idAttElems = new HashSet<String>();
+        Set<String> idAttElems = new HashSet<>();
 
         for (String elemName : DOCUMENT_STRUCTURE_ELEMENTS) {
             idAttElems.add(elemName);
@@ -335,7 +351,7 @@
      */
     private static final Set<String> PREFACE_LIKE_ELEMENTS;
     static {
-        Set<String> sinlgeFileElems = new HashSet<String>();
+        Set<String> sinlgeFileElems = new HashSet<>();
 
         sinlgeFileElems.add(E_PREFACE);
 
@@ -350,6 +366,8 @@
     // -------------------------------------------------------------------------
     // Settings:
 
+    private File cfgFile;
+
     private File destDir;
 
     private File srcDir;
@@ -370,11 +388,9 @@
     private Set<String> removeNodesWhenOnline;
 
     /** Element types for which a new output file is created  */
-    private DocumentStructureRank lowestFileElemenRank
-            = DocumentStructureRank.SECTION1;
+    private DocumentStructureRank lowestFileElemenRank = DocumentStructureRank.SECTION1;
 
-    private DocumentStructureRank lowestPageTOCElemenRank
-            = DocumentStructureRank.SECTION3;
+    private DocumentStructureRank lowestPageTOCElemenRank = DocumentStructureRank.SECTION3;
 
     private int maxTOFDisplayDepth = Integer.MAX_VALUE;
 
@@ -402,24 +418,24 @@
 
     private boolean printProgress;
 
-    private LinkedHashMap<String, String> internalBookmarks = new LinkedHashMap<>();
-    private LinkedHashMap<String, String> externalBookmarks = new LinkedHashMap<>();
-    private Map<String, Map<String, String>> footerSiteMap;
+    private final LinkedHashMap<String, String> internalBookmarks = new LinkedHashMap<>();
+    private final LinkedHashMap<String, String> externalBookmarks = new LinkedHashMap<>();
+    private Map<String, Map<String, String>> footerSiteMap = new LinkedHashMap<>();;
 
-    private Map<String, Object> customVariablesFromSettingsFile = new HashMap<>();
-    private Map<String, Object> customVariableOverrides = new HashMap<>();
+    private final Map<String, Object> customVariablesFromSettingsFile = new HashMap<>();
+    private final Map<String, Object> customVariableOverrides = new HashMap<>();
 
-    private Map<String, String> insertableFilesFromSettingsFile = new HashMap<>();
-    private Map<String, String> insertableFilesOverrides = new HashMap<>();
+    private final Map<String, String> insertableFilesFromSettingsFile = new HashMap<>();
+    private final Map<String, String> insertableFilesOverrides = new HashMap<>();
 
-    private LinkedHashMap<String, String> tabs = new LinkedHashMap<>();
+    private final LinkedHashMap<String, String> tabs = new LinkedHashMap<>();
 
-    private Map<String, Map<String, String>> secondaryTabs;
-    private Map<String, Map<String, String>> socialLinks;
+    private final Map<String, Map<String, String>> secondaryTabs = new LinkedHashMap<>();
+    private final Map<String, Map<String, String>> socialLinks = new LinkedHashMap<>();
 
     private Logo logo;
 
-    private List<Logo> sideTOCLogos;
+    private final List<Logo> sideTOCLogos = new ArrayList<>();
 
     private String copyrightHolder;
     private String copyrightHolderSite;
@@ -428,20 +444,20 @@
     private String copyrightComment;
     private String copyrightJavaComment;
 
-    private Map<String, Map<String, String>> seoMeta;
+    private final Map<String, Map<String, String>> seoMeta = new LinkedHashMap();
 
-    private DocgenValidationOptions validationOps
-            = new DocgenValidationOptions();
+    private DocgenValidationOptions validationOps = new DocgenValidationOptions();
+
+    String eclipseLinkTo;
 
     // -------------------------------------------------------------------------
     // Global transformation state:
 
     private boolean executed;
 
-    private Map<String, String> olinks = new HashMap<String, String>();
+    private Map<String, String> olinks = new HashMap<>();
     private Map<String, List<NodeModel>> primaryIndexTermLookup;
-    private Map<String, SortedMap<String, List<NodeModel>>>
-            secondaryIndexTermLookup;
+    private Map<String, SortedMap<String, List<NodeModel>>> secondaryIndexTermLookup;
     private Map<String, Element> elementsById;
     private List<TOCNode> tocNodes;
     private List<String> indexEntries;
@@ -458,12 +474,14 @@
 
     private DocgenLogger logger = new DocgenLogger() {
 
+        @Override
         public void info(String message) {
             if (printProgress) {
                 System.out.println(message);
             }
         }
 
+        @Override
         public void warning(String message) {
             if (printProgress) {
                 System.out.println("Warning:" + message);
@@ -514,9 +532,8 @@
         // Load configuration file:
 
         File templatesDir = null;
-        String eclipseLinkTo = null;
 
-        File cfgFile = new File(srcDir, FILE_SETTINGS);
+        cfgFile = new File(srcDir, FILE_SETTINGS);
         if (cfgFile.exists()) {
             Map<String, Object> cfg;
             try {
@@ -527,181 +544,128 @@
             }
 
             for (Entry<String, Object> cfgEnt : cfg.entrySet()) {
-                final String settingName = cfgEnt.getKey();
+                final String topSettingName = cfgEnt.getKey();
+                final SettingName settingName = SettingName.topLevel(cfgFile, topSettingName);
                 final Object settingValue = cfgEnt.getValue();
 
-                if (settingName.equals(SETTING_IGNORED_FILES)) {
-                    List<String> patterns = castSettingToStringList(cfgFile, settingName, settingValue);
-                    for (String pattern : patterns) {
-                        ignoredFilePathPatterns.add(FileUtil.globToRegexp(pattern));
-                    }
-                } else if (settingName.equals(SETTING_OLINKS)) {
-                    Map<String, Object> m = castSettingToMapWithStringKeys(
-                            cfgFile, settingName, settingValue);
-                    for (Entry<String, Object> ent : m.entrySet()) {
-                        String name = ent.getKey();
-                        String target = castSettingValueMapValueToString(
-                                cfgFile, settingName, ent.getValue());
-                        olinks.put(name, target);
-                    }
-                } else if (settingName.equals(SETTING_INTERNAL_BOOKMARKS)) {
-                    Map<String, Object> m = castSettingToMapWithStringKeys(
-                            cfgFile, settingName, settingValue);
-                    for (Entry<String, Object> ent : m.entrySet()) {
-                        String name = ent.getKey();
-                        String target = castSettingValueMapValueToString(
-                                cfgFile, settingName, ent.getValue());
-                        internalBookmarks.put(name, target);
-                    }
+                if (topSettingName.equals(SETTING_IGNORED_FILES)) {
+                    castSettingToList(settingName, settingValue, String.class).forEach(
+                            pattern -> ignoredFilePathPatterns.add(FileUtil.globToRegexp(pattern)));
+                } else if (topSettingName.equals(SETTING_OLINKS)) {
+                    olinks.putAll(
+                            castSettingToMap(settingName, settingValue, String.class, String.class));
+                } else if (topSettingName.equals(SETTING_INTERNAL_BOOKMARKS)) {
+                    internalBookmarks.putAll(
+                            castSettingToMap(settingName, settingValue, String.class, String.class));
                     // Book-mark targets will be checked later, when the XML
                     // document is already loaded.
-                } else if (settingName.equals(SETTING_EXTERNAL_BOOKMARKS)) {
-                    Map<String, Object> m = castSettingToMapWithStringKeys(
-                            cfgFile, settingName, settingValue);
-                    for (Entry<String, Object> ent : m.entrySet()) {
-                        String name = ent.getKey();
-                        String target = castSettingValueMapValueToString(
-                                cfgFile, settingName, ent.getValue());
-                        externalBookmarks.put(name, target);
-                    }
-                } else if (settingName.equals(SETTING_LOGO)) {
-                    Map<String, Object> m = castSettingToMapWithStringKeys(
-                            cfgFile, settingName, settingValue);
-                    logo = castMapToLogo(cfgFile, settingName, m);
-                } else if (settingName.equals(SETTING_SIDE_TOC_LOGOS)) {
-                    List<Map<String, Object>> listOfMaps
-                            = castSettingToListOfMapsWithStringKeys(cfgFile, settingName, settingValue);
-                    sideTOCLogos = new ArrayList<>();
+                } else if (topSettingName.equals(SETTING_EXTERNAL_BOOKMARKS)) {
+                    externalBookmarks.putAll(
+                            castSettingToMap(settingName, settingValue, String.class, String.class));
+                } else if (topSettingName.equals(SETTING_LOGO)) {
+                    logo = castMapToLogo(settingName, settingValue);
+                } else if (topSettingName.equals(SETTING_SIDE_TOC_LOGOS)) {
+                    List<Map<String, Object>> listOfMaps = castSetting(
+                            settingName, settingValue,
+                            List.class,
+                            new ListItemType(Map.class),
+                            new MapEntryType<>(String.class, Object.class));
                     for (int i = 0; i < listOfMaps.size(); i++) {
-                        Map<String, Object> map = listOfMaps.get(i);
-                        sideTOCLogos.add(castMapToLogo(cfgFile, settingName + "[" + i + "]", map));
+                        sideTOCLogos.add(castMapToLogo(settingName.subKey(i), listOfMaps.get(i)));
                     }
-                } else if (settingName.equals(SETTING_COPYRIGHT_HOLDER)) {
-                    copyrightHolder = castSettingToString(cfgFile, settingName, settingValue);
-                } else if (settingName.equals(SETTING_COPYRIGHT_HOLDER_SITE)) {
-                    copyrightHolderSite = castSettingToString(cfgFile, settingName, settingValue);
-                } else if (settingName.equals(SETTING_COPYRIGHT_START_YEAR)) {
-                    copyrightStartYear = castSettingToInt(cfgFile, settingName, settingValue);
-                } else if (settingName.equals(SETTING_COPYRIGHT_SUFFIX)) {
-                    copyrightSuffix = castSettingToString(cfgFile, settingName, settingValue);
-                } else if (settingName.equals(SETTING_COPYRIGHT_COMMENT_FILE)) {
-                    copyrightComment = StringUtil.chomp(getFileContentForSetting(cfgFile, settingName, settingValue));
+                } else if (topSettingName.equals(SETTING_COPYRIGHT_HOLDER)) {
+                    copyrightHolder = castSetting(settingName, settingValue, String.class);
+                } else if (topSettingName.equals(SETTING_COPYRIGHT_HOLDER_SITE)) {
+                    copyrightHolderSite = castSetting(settingName, settingValue, String.class);
+                } else if (topSettingName.equals(SETTING_COPYRIGHT_START_YEAR)) {
+                    copyrightStartYear = castSetting(settingName, settingValue, Integer.class);
+                } else if (topSettingName.equals(SETTING_COPYRIGHT_SUFFIX)) {
+                    copyrightSuffix = castSetting(settingName, settingValue, String.class);
+                } else if (topSettingName.equals(SETTING_COPYRIGHT_COMMENT_FILE)) {
+                    copyrightComment =
+                            StringUtil.chomp(getFileContentForSetting(settingName, settingValue));
                     String eol = TextUtil.detectEOL(copyrightComment, "\n");
                     StringBuilder sb = new StringBuilder("/*").append(eol);
                     new BufferedReader(new StringReader(copyrightComment)).lines()
                             .forEach(s -> sb.append(" * ").append(s).append(eol));
                     sb.append(" */");
                     copyrightJavaComment = sb.toString();
-                } else if (settingName.equals(SETTING_SEO_META)) {
-                    Map<String, Object> m = castSettingToMapWithStringKeys(
-                            cfgFile, settingName, settingValue);
-                    seoMeta = new LinkedHashMap<>();
-                    for (Entry<String, Object> ent : m.entrySet()) {
-                        String k = ent.getKey();
-                        Map<String, String> v = castSettingValueMapValueToMapOfStringString(
-                                cfgFile, settingName, ent.getValue(),
-                                null, SETTING_SEO_META_KEYS);
-                        seoMeta.put(k, v);
-                    }
-                } else if (settingName.equals(SETTING_CUSTOM_VARIABLES)) {
+                } else if (topSettingName.equals(SETTING_SEO_META)) {
+                    this.seoMeta.putAll(
+                            castSetting(
+                                    settingName, settingValue,
+                                    Map.class,
+                                    new MapEntryType<>(String.class, Map.class),
+                                    new MapEntryType<>(
+                                            String.class, Collections.emptySet(), SETTING_SEO_META_KEYS,
+                                            String.class)));
+                } else if (topSettingName.equals(SETTING_CUSTOM_VARIABLES)) {
                     customVariablesFromSettingsFile.putAll(
-                            castSettingToMapWithStringKeys(cfgFile, settingName, settingValue));
-                } else if (settingName.equals(SETTING_INSERTABLE_FILES)) {
-                    Map<String, Object> m = castSettingToMapWithStringKeys(
-                            cfgFile, settingName, settingValue);
-                    for (Entry<String, Object> ent : m.entrySet()) {
-                        String value = castSettingValueMapValueToString(cfgFile, settingName, ent.getValue());
-                        if (insertableFilesFromSettingsFile.put(ent.getKey(), value) != null) {
-                            throw new DocgenException(
-                                    "Duplicate key " + StringUtil.jQuote(ent.getKey()) + " in "
-                                            + SETTING_INSERTABLE_FILES + ".");
-                        }
-                    }
-                } else if (settingName.equals(SETTING_TABS)) {
-                    Map<String, Object> m = castSettingToMapWithStringKeys(
-                            cfgFile, settingName, settingValue);
-                    for (Entry<String, Object> ent : m.entrySet()) {
-                        String k = ent.getKey();
-                        String v = castSettingValueMapValueToString(cfgFile, settingName, ent.getValue());
-                        tabs.put(k, v);
-                    }
-                } else if (settingName.equals(SETTING_SECONDARY_TABS)) {
-                    Map<String, Object> m = castSettingToMapWithStringKeys(
-                            cfgFile, settingName, settingValue);
-                    secondaryTabs = new LinkedHashMap<>();
-                    for (Entry<String, Object> ent : m.entrySet()) {
-                        String k = ent.getKey();
-                        Map<String, String> v = castSettingValueMapValueToMapOfStringString(
-                                cfgFile, settingName, ent.getValue(),
-                                COMMON_LINK_KEYS, null);
-                        secondaryTabs.put(k, v);
-                    }
-                } else if (settingName.equals(SETTING_SOCIAL_LINKS)) {
-                    Map<String, Object> m = castSettingToMapWithStringKeys(
-                            cfgFile, settingName, settingValue);
-                    socialLinks = new LinkedHashMap<>();
-                    for (Entry<String, Object> ent : m.entrySet()) {
-                        String entName = ent.getKey();
-                        Map<String, String> entValue = castSettingValueMapValueToMapOfStringString(
-                                cfgFile, settingName, ent.getValue(),
-                                COMMON_LINK_KEYS, null);
-                        socialLinks.put(entName, entValue);
-                    }
-                } else if (settingName.equals(SETTING_FOOTER_SITEMAP)) {
-                    // TODO Check value in more details
-                    footerSiteMap = (Map) castSettingToMapWithStringKeys(
-                            cfgFile, settingName, settingValue);
-                }else if (settingName.equals(SETTING_VALIDATION)) {
-                    Map<String, Object> m = castSettingToMapWithStringKeys(
-                            cfgFile, SETTING_VALIDATION, settingValue);
-                    for (Entry<String, Object> ent : m.entrySet()) {
-                        String name = ent.getKey();
-                        if (name.equals(
-                                SETTING_VALIDATION_PROGRAMLISTINGS_REQ_ROLE)) {
-                            validationOps.setProgramlistingRequiresRole(
-                                    caseSettingToBoolean(
-                                            cfgFile,
-                                            settingName + "." + name,
-                                            ent.getValue()));
-                        } else if (name.equals(
-                                SETTING_VALIDATION_PROGRAMLISTINGS_REQ_LANG)) {
-                            validationOps.setProgramlistingRequiresLanguage(
-                                    caseSettingToBoolean(
-                                            cfgFile,
-                                            settingName + "." + name,
-                                            ent.getValue()));
-                        } else if (name.equals(
-                                SETTING_VALIDATION_OUTPUT_FILES_CAN_USE_AUTOID)
+                            // Allow null values in the Map, as the caller can override them.
+                            castSettingToMap(settingName, settingValue, String.class, Object.class, true));
+                } else if (topSettingName.equals(SETTING_INSERTABLE_FILES)) {
+                    insertableFilesFromSettingsFile.putAll(
+                            // Allow null values in the Map, as the caller can override them.
+                            castSettingToMap(settingName, settingValue, String.class, String.class, true));
+                } else if (topSettingName.equals(SETTING_TABS)) {
+                    tabs.putAll(
+                            castSettingToMap(settingName, settingValue, String.class, String.class));
+                } else if (topSettingName.equals(SETTING_SECONDARY_TABS)) {
+                    secondaryTabs.putAll(
+                            castSetting(
+                                    settingName, settingValue,
+                                    Map.class,
+                                    new MapEntryType(String.class, Map.class),
+                                    new MapEntryType(String.class, COMMON_LINK_KEYS, String.class)));
+                } else if (topSettingName.equals(SETTING_SOCIAL_LINKS)) {
+                    socialLinks.putAll(
+                            castSetting(
+                                    settingName, settingValue,
+                                    Map.class,
+                                    new MapEntryType(String.class, Map.class),
+                                    new MapEntryType(String.class, COMMON_LINK_KEYS, String.class)));
+                } else if (topSettingName.equals(SETTING_FOOTER_SITEMAP)) {
+                    footerSiteMap.putAll(
+                            castSetting(
+                                    settingName, settingValue,
+                                    Map.class,
+                                    new MapEntryType(String.class, Map.class),
+                                    new MapEntryType(String.class, String.class)));
+                }else if (topSettingName.equals(SETTING_VALIDATION)) {
+                    castSettingToMap(settingName, settingValue, String.class, Object.class)
+                            .forEach((name, value) -> {
+                                if (name.equals(
+                                        SETTING_VALIDATION_PROGRAMLISTINGS_REQ_ROLE)) {
+                                    validationOps.setProgramlistingRequiresRole(
+                                            castSetting(settingName.subKey(name), value, Boolean.class));
+                                } else if (name.equals(
+                                        SETTING_VALIDATION_PROGRAMLISTINGS_REQ_LANG)) {
+                                    validationOps.setProgramlistingRequiresLanguage(
+                                            castSetting(settingName.subKey(name), value, Boolean.class));
+                                } else if (name.equals(
+                                        SETTING_VALIDATION_OUTPUT_FILES_CAN_USE_AUTOID)
                                 ) {
-                            validationOps.setOutputFilesCanUseAutoID(
-                                    caseSettingToBoolean(
-                                            cfgFile,
-                                            settingName + "." + name,
-                                            ent.getValue()));
-                        } else if (name.equals(
-                                SETTING_VALIDATION_MAXIMUM_PROGRAMLISTING_WIDTH)
+                                    validationOps.setOutputFilesCanUseAutoID(
+                                            castSetting(settingName.subKey(name), value, Boolean.class));
+                                } else if (name.equals(
+                                        SETTING_VALIDATION_MAXIMUM_PROGRAMLISTING_WIDTH)
                                 ) {
-                            validationOps.setMaximumProgramlistingWidth(
-                                    castSettingToInt(
-                                            cfgFile,
-                                            settingName + "." + name,
-                                            ent.getValue()));
-                        } else {
-                            throw newCfgFileException(
-                                    cfgFile, SETTING_VALIDATION,
-                                    "Unknown validation option: " + name);
-                        }
-                    }
-                } else if (settingName.equals(SETTING_OFFLINE)) {
+                                    validationOps.setMaximumProgramlistingWidth(
+                                            castSetting(settingName.subKey(name), value, Integer.class));
+                                } else {
+                                    throw newCfgFileException(settingName.subKey(name), "Unknown validation option: " + name);
+                                }
+                            });
+                } else if (topSettingName.equals(SETTING_OFFLINE)) {
                     if (offline == null) {  // Ignore if the caller has already set this
-                        offline = caseSettingToBoolean(cfgFile, settingName, settingValue);
+                        offline = castSetting(settingName, settingValue, Boolean.class);
                     }
-                } else if (settingName.equals(SETTING_SIMPLE_NAVIGATION_MODE)) {
-                    simpleNavigationMode = caseSettingToBoolean(cfgFile, settingName, settingValue);
-                } else if (settingName.equals(SETTING_DEPLOY_URL)) {
-                    deployUrl = castSettingToString(cfgFile, settingName, settingValue);
-                } else if (settingName.equals(SETTING_ONLINE_TRACKER_HTML)) {
-                    onlineTrackerHTML = getFileContentForSetting(cfgFile, settingName, settingValue);
+                } else if (topSettingName.equals(SETTING_SIMPLE_NAVIGATION_MODE)) {
+                    simpleNavigationMode = castSetting(settingName, settingValue, Boolean.class);
+                } else if (topSettingName.equals(SETTING_DEPLOY_URL)) {
+                    deployUrl = castSetting(settingName, settingValue, String.class);
+                } else if (topSettingName.equals(SETTING_ONLINE_TRACKER_HTML)) {
+                    onlineTrackerHTML = getFileContentForSetting(settingName, settingValue);
                     if (onlineTrackerHTML.startsWith("<!--")) {
                         int commentEnd = onlineTrackerHTML.indexOf("-->");
                         if (commentEnd != -1) {
@@ -715,66 +679,49 @@
                     String eol = TextUtil.detectEOL(onlineTrackerHTML, "\n");
                     onlineTrackerHTML = onlineTrackerHTML.trim();
                     onlineTrackerHTML += eol;
-                } else if (settingName.equals(SETTING_COOKIE_CONSENT_SCRIPT_URL)) {
-                    cookieConstentScriptURL = castSettingToString(cfgFile, settingName, settingValue);
-                } else if (settingName.equals(SETTING_REMOVE_NODES_WHEN_ONLINE)) {
-                    removeNodesWhenOnline = Collections.unmodifiableSet(new HashSet<String>(
-                            castSettingToStringList(cfgFile, settingName, settingValue)));
-                } else if (settingName.equals(SETTING_ECLIPSE)) {
-                    Map<String, Object> m = castSettingToMapWithStringKeys(
-                            cfgFile, settingName, settingValue);
-                    for (Entry<String, Object> ent : m.entrySet()) {
-                        String name = ent.getKey();
-                        if (name.equals(SETTING_ECLIPSE_LINK_TO)) {
-                            String value = castSettingToString(
-                                    cfgFile,
-                                    settingName + "." + name,
-                                    ent.getValue());
-                            eclipseLinkTo = value;
-                        } else {
-                            throw newCfgFileException(
-                                    cfgFile, settingName,
-                                    "Unknown Eclipse option: " + name);
-                        }
-                    }
-                } else if (settingName.equals(SETTING_LOCALE)) {
-                    String s = castSettingToString(
-                            cfgFile, settingName, settingValue);
+                } else if (topSettingName.equals(SETTING_COOKIE_CONSENT_SCRIPT_URL)) {
+                    cookieConstentScriptURL = castSetting(settingName, settingValue, String.class);
+                } else if (topSettingName.equals(SETTING_REMOVE_NODES_WHEN_ONLINE)) {
+                    removeNodesWhenOnline = Collections.unmodifiableSet(new HashSet<>(
+                            castSettingToList(settingName, settingValue, String.class)));
+                } else if (topSettingName.equals(SETTING_ECLIPSE)) {
+                    castSettingToMap(settingName, settingValue, String.class, Object.class)
+                            .forEach((name, value) -> {
+                                if (name.equals(SETTING_ECLIPSE_LINK_TO)) {
+                                    eclipseLinkTo = castSetting(
+                                            settingName.subKey(name), value, String.class);
+                                } else {
+                                    throw newCfgFileException(settingName, "Unknown Eclipse option: " + name);
+                                }
+                            });
+                } else if (topSettingName.equals(SETTING_LOCALE)) {
+                    String s = castSetting(settingName, settingValue, String.class);
                     locale = StringUtil.deduceLocale(s);
-                } else if (settingName.equals(SETTING_TIME_ZONE)) {
-                    String s = castSettingToString(
-                            cfgFile, settingName, settingValue);
+                } else if (topSettingName.equals(SETTING_TIME_ZONE)) {
+                    String s = castSetting(settingName, settingValue, String.class);
                     timeZone = TimeZone.getTimeZone(s);
-                } else if (settingName.equals(SETTING_GENERATE_ECLIPSE_TOC)) {
-                    generateEclipseTOC = caseSettingToBoolean(
-                            cfgFile, settingName, settingValue);
-                } else if (settingName.equals(SETTING_SHOW_EDITORAL_NOTES)) {
-                    showEditoralNotes = caseSettingToBoolean(
-                            cfgFile, settingName, settingValue);
-                } else if (settingName.equals(SETTING_SHOW_XXE_LOGO)) {
-                    showXXELogo = caseSettingToBoolean(
-                            cfgFile, settingName, settingValue);
-                } else if (settingName.equals(SETTING_SEARCH_KEY)) {
-                    searchKey = castSettingToString(
-                            cfgFile, settingName, settingValue);
-                }else if (settingName.equals(SETTING_DISABLE_JAVASCRIPT)) {
-                    disableJavaScript = caseSettingToBoolean(
-                            cfgFile, settingName, settingValue);
-                } else if (settingName.equals(SETTING_CONTENT_DIRECTORY)) {
-                    String s = castSettingToString(
-                            cfgFile, settingName, settingValue);
+                } else if (topSettingName.equals(SETTING_GENERATE_ECLIPSE_TOC)) {
+                    generateEclipseTOC = castSetting(settingName, settingValue, Boolean.class);
+                } else if (topSettingName.equals(SETTING_SHOW_EDITORAL_NOTES)) {
+                    showEditoralNotes = castSetting(settingName, settingValue, Boolean.class);
+                } else if (topSettingName.equals(SETTING_SHOW_XXE_LOGO)) {
+                    showXXELogo = castSetting(settingName, settingValue, Boolean.class);
+                } else if (topSettingName.equals(SETTING_SEARCH_KEY)) {
+                    searchKey = castSetting(settingName, settingValue, String.class);
+                }else if (topSettingName.equals(SETTING_DISABLE_JAVASCRIPT)) {
+                    disableJavaScript = castSetting(settingName, settingValue, Boolean.class);
+                } else if (topSettingName.equals(SETTING_CONTENT_DIRECTORY)) {
+                    String s = castSetting(settingName, settingValue, String.class);
                     contentDir = new File(srcDir, s);
                     if (!contentDir.isDirectory()) {
-                        throw newCfgFileException(cfgFile, settingName,
-                                "It's not an existing directory: "
-                                + contentDir.getAbsolutePath());
+                        throw newCfgFileException(
+                                settingName,
+                                "It's not an existing directory: " + contentDir.getAbsolutePath());
                     }
-                } else if (settingName.equals(SETTING_LOWEST_FILE_ELEMENT_RANK)
-                        || settingName.equals(
-                                SETTING_LOWEST_PAGE_TOC_ELEMENT_RANK)) {
+                } else if (topSettingName.equals(SETTING_LOWEST_FILE_ELEMENT_RANK)
+                        || topSettingName.equals(SETTING_LOWEST_PAGE_TOC_ELEMENT_RANK)) {
                     DocumentStructureRank rank;
-                    String strRank = castSettingToString(
-                            cfgFile, settingName, settingValue);
+                    String strRank = castSetting(settingName, settingValue, String.class);
                     try {
                         rank = DocumentStructureRank.valueOf(
                                 strRank.toUpperCase());
@@ -789,44 +736,30 @@
                         } else {
                             msg = "Unknown rank: " + strRank;
                         }
-                        throw newCfgFileException(cfgFile, settingName,
-                                msg);
+                        throw newCfgFileException(settingName, msg);
                     }
 
-                    if (settingName.equals(
-                            SETTING_LOWEST_FILE_ELEMENT_RANK)) {
+                    if (topSettingName.equals(SETTING_LOWEST_FILE_ELEMENT_RANK)) {
                         lowestFileElemenRank = rank;
-                    } else if (settingName.equals(
-                            SETTING_LOWEST_PAGE_TOC_ELEMENT_RANK)) {
+                    } else if (topSettingName.equals(SETTING_LOWEST_PAGE_TOC_ELEMENT_RANK)) {
                         lowestPageTOCElemenRank = rank;
                     } else {
                         throw new BugException("Unexpected setting name.");
                     }
-                } else if (settingName.equals(SETTING_MAX_TOF_DISPLAY_DEPTH)) {
-                    maxTOFDisplayDepth = castSettingToInt(
-                            cfgFile, settingName, settingValue);
+                } else if (topSettingName.equals(SETTING_MAX_TOF_DISPLAY_DEPTH)) {
+                    maxTOFDisplayDepth = castSetting(settingName, settingValue, Integer.class);
                     if (maxTOFDisplayDepth < 1) {
-                        throw newCfgFileException(cfgFile, settingName,
-                                "Value must be at least 1.");
+                        throw newCfgFileException(settingName, "Value must be at least 1.");
                     }
-                } else if (settingName.equals(
-                        SETTING_MAX_MAIN_TOF_DISPLAY_DEPTH)) {
-                    maxMainTOFDisplayDepth = castSettingToInt(
-                            cfgFile, settingName, settingValue);
+                } else if (topSettingName.equals(SETTING_MAX_MAIN_TOF_DISPLAY_DEPTH)) {
+                    maxMainTOFDisplayDepth = castSetting(settingName, settingValue, Integer.class);
                     if (maxTOFDisplayDepth < 1) {
-                        throw newCfgFileException(cfgFile, settingName,
-                                "Value must be at least 1.");
+                        throw newCfgFileException(settingName, "Value must be at least 1.");
                     }
-                } else if (settingName.equals(SETTING_NUMBERED_SECTIONS)) {
-                    numberedSections = caseSettingToBoolean(
-                            cfgFile, settingName, settingValue);
+                } else if (topSettingName.equals(SETTING_NUMBERED_SECTIONS)) {
+                    numberedSections = castSetting(settingName, settingValue, Boolean.class);
                 } else {
-                    throw newCfgFileException(cfgFile, "Unknown setting: \""
-                            + settingName
-                            + "\". (Hint: See the list of available "
-                            + "settings in the Java API documentation of "
-                            + Transform.class.getName() + ". Also, note that "
-                            + "setting names are case-sensitive.)");
+                    throw newCfgFileException(settingName, "Unknown setting name.");
                 }
             } // for each cfg settings
 
@@ -879,12 +812,11 @@
 
         // Initialize state fields
 
-        primaryIndexTermLookup = new HashMap<String, List<NodeModel>>();
-        secondaryIndexTermLookup
-                = new HashMap<String, SortedMap<String, List<NodeModel>>>();
-        elementsById = new HashMap<String, Element>();
-        tocNodes = new ArrayList<TOCNode>();
-        indexEntries = new ArrayList<String>();
+        primaryIndexTermLookup = new HashMap<>();
+        secondaryIndexTermLookup = new HashMap<>();
+        elementsById = new HashMap<>();
+        tocNodes = new ArrayList<>();
+        indexEntries = new ArrayList<>();
 
         // Setup FreeMarker:
 
@@ -948,35 +880,27 @@
                 tabEnt.setValue(resolveDocgenURL(SETTING_TABS, tabEnt.getValue()));
             }
         }
-        if (secondaryTabs != null) {
-            for (Map<String, String> tab : secondaryTabs.values()) {
-                tab.put("href", resolveDocgenURL(SETTING_SECONDARY_TABS, tab.get("href")));
-            }
+        for (Map<String, String> secondaryTab : secondaryTabs.values()) {
+            secondaryTab.put("href", resolveDocgenURL(SETTING_SECONDARY_TABS, secondaryTab.get("href")));
         }
         if (externalBookmarks != null) {
             for (Entry<String, String> bookmarkEnt : externalBookmarks.entrySet()) {
                 bookmarkEnt.setValue(resolveDocgenURL(SETTING_EXTERNAL_BOOKMARKS, bookmarkEnt.getValue()));
             }
         }
-        if (socialLinks != null) {
-            for (Map<String, String> tab : socialLinks.values()) {
-                tab.put("href", resolveDocgenURL(SETTING_SOCIAL_LINKS, tab.get("href")));
-            }
+        for (Map<String, String> tab : socialLinks.values()) {
+            tab.put("href", resolveDocgenURL(SETTING_SOCIAL_LINKS, tab.get("href")));
         }
-        if (footerSiteMap != null) {
-            for (Map<String, String> links : footerSiteMap.values()) {
-                for (Map.Entry<String, String> link : links.entrySet()) {
-                    link.setValue(resolveDocgenURL(SETTING_FOOTER_SITEMAP, link.getValue()));
-                }
+        for (Map<String, String> links : footerSiteMap.values()) {
+            for (Map.Entry<String, String> link : links.entrySet()) {
+                link.setValue(resolveDocgenURL(SETTING_FOOTER_SITEMAP, link.getValue()));
             }
         }
         if (logo != null) {
             resolveLogoHref(logo);
         }
-        if (sideTOCLogos != null) {
-            for (Logo logo : sideTOCLogos) {
-                resolveLogoHref(logo);
-            }
+        for (Logo logo : sideTOCLogos) {
+            resolveLogoHref(logo);
         }
 
         // - Create destination directory:
@@ -990,10 +914,9 @@
         for (Entry<String, String> ent : internalBookmarks.entrySet()) {
             String id = ent.getValue();
             if (!elementsById.containsKey(id)) {
-                throw newCfgFileException(cfgFile,
-                        SETTING_INTERNAL_BOOKMARKS,
-                        "No element with id \"" + id
-                        + "\" exists in the book.");
+                throw newCfgFileException(
+                        SettingName.topLevel(cfgFile, SETTING_INTERNAL_BOOKMARKS),
+                        "No element with id \"" + id + "\" exists in the book.");
             }
         }
 
@@ -1154,8 +1077,8 @@
                     }
                 } catch (freemarker.core.StopException e) {
                     throw new DocgenException(e.getMessage());
-                } catch (DocgenSubstitutionTemplateException e) {
-                    throw new DocgenException("Docgen substitution in document text failed; see cause exception", e);
+                } catch (DocgenTagException e) {
+                    throw new DocgenException("Docgen tag evaluation in document text failed; see cause exception", e);
                 } catch (TemplateException e) {
                     throw new BugException(e);
                 }
@@ -1338,286 +1261,37 @@
         }
     }
 
-    private DocgenException newCfgFileException(
-            File cfgFile, String settingName, String desc) {
-        settingName = settingName.replace(".", "\" per \"");
-        return newCfgFileException(cfgFile, "Wrong value for setting \""
-                + settingName + "\": " + desc);
+    private static Logo castMapToLogo(SettingName settingName, Object settingValue) {
+        Map<String, String> logoMap = castSetting(
+                settingName,
+                settingValue, false,
+                Map.class,
+                new MapEntryType(String.class, SETTING_LOGO_MAP_KEYS, String.class));
+        return new Logo(
+                logoMap.get(SETTING_LOGO_KEY_SRC),
+                logoMap.get(SETTING_LOGO_KEY_ALT),
+                logoMap.get(SETTING_LOGO_KEY_HREF));
     }
 
-    private DocgenException newCfgFileException(File cfgFile, String desc) {
-        return newCfgFileException(cfgFile, desc, (Throwable) null);
-    }
-
-    private DocgenException newCfgFileException(File cfgFile, String desc,
-            Throwable cause) {
-        StringBuilder sb = new StringBuilder();
-        sb.append("Wrong configuration");
-        if (cfgFile != null) {
-            sb.append(" file \"");
-            sb.append(cfgFile.getAbsolutePath());
-            sb.append("\"");
-        }
-        sb.append(": ");
-        sb.append(desc);
-        return new DocgenException(sb.toString(), cause);
-    }
-
-    @SuppressWarnings("unchecked")
-    private Map<String, Object> castSettingToMapWithStringKeys(
-            File cfgFile, String settingName, Object settingValue)
-            throws DocgenException {
-        if (!(settingValue instanceof Map)) {
-            throw newCfgFileException(
-                    cfgFile, settingName,
-                    "Should be a map (like { key1: value1, key2: value2 }), but "
-                    + "it's a " + CJSONInterpreter.cjsonTypeOf(settingValue)
-                    + ".");
-        }
-        for (Object key : ((Map<?, ?>) settingValue).keySet()) {
-            if (!(key instanceof String)) {
-                throw newCfgFileException(
-                        cfgFile, settingName,
-                        "All keys should be String-s, but one of them is a(n) "
-                        + CJSONInterpreter.cjsonTypeOf(settingValue) + ".");
-            }
-        }
-        return (Map<String, Object>) settingValue;
-    }
-
-    @SuppressWarnings("unchecked")
-    private List<String> castSettingToStringList(
-            File cfgFile, String settingName, Object settingValue)
-            throws DocgenException {
-        List<?> settingValueAsList = castSettingToList(cfgFile, settingName, settingValue);
-        for (int i = 0; i < settingValueAsList.size(); i++) {
-            Object listItem = settingValueAsList.get(i);
-            if (!(listItem instanceof String)) {
-                throw newCfgFileException(
-                        cfgFile, settingName,
-                        "Should be a list of String-s (like [\"value1\", \"value2\", ... \"valueN\"]), but at index "
-                        + i +" (0-based) there's a " + CJSONInterpreter.cjsonTypeOf(listItem)
-                        + ".");
-            }
-        }
-        return (List<String>) settingValue;
-    }
-
-    @SuppressWarnings("unchecked")
-    private List<Map<String, Object>> castSettingToListOfMapsWithStringKeys(
-            File cfgFile, String settingName, Object settingValue)
-            throws DocgenException {
-        List<?> settingValueAsList = castSettingToList(cfgFile, settingName, settingValue);
-        for (int i = 0; i < settingValueAsList.size(); i++) {
-            castSettingToMapWithStringKeys(cfgFile, settingName + "[" + i + "]", settingValueAsList.get(i));
-        }
-        return (List) settingValue;
-    }
-
-    private List<?> castSettingToList(File cfgFile, String settingName, Object settingValue) throws DocgenException {
-        if (!(settingValue instanceof List)) {
-            throw newCfgFileException(
-                    cfgFile, settingName,
-                    "Should be a list (like [value1, value2, ... valueN]), but "
-                    + "it's a " + CJSONInterpreter.cjsonTypeOf(settingValue)
-                    + ".");
-        }
-        return (List<?>) settingValue;
-    }
-
-    private String castSettingToString(File cfgFile,
-            String settingName, Object settingValue) throws DocgenException {
-        if (!(settingValue instanceof String)) {
-            throw newCfgFileException(
-                    cfgFile, settingName,
-                    "Should be a string, but it's a "
-                    + CJSONInterpreter.cjsonTypeOf(settingValue) + ".");
-        }
-        return (String) settingValue;
-    }
-
-    private boolean caseSettingToBoolean(File cfgFile,
-            String settingName, Object settingValue) throws DocgenException {
-        if (!(settingValue instanceof Boolean)) {
-            throw newCfgFileException(
-                    cfgFile, settingName,
-                    "Should be a boolean (i.e., true or false), but it's a "
-                    + CJSONInterpreter.cjsonTypeOf(settingValue) + ".");
-        }
-        return (Boolean) settingValue;
-    }
-
-    private int castSettingToInt(File cfgFile,
-            String settingName, Object settingValue)
-            throws DocgenException {
-
-        if (!(settingValue instanceof Number)) {
-            throw newCfgFileException(
-                    cfgFile, settingName,
-                    "Should be an number, but it's a "
-                    + CJSONInterpreter.cjsonTypeOf(settingValue) + ".");
-        }
-        if (!(settingValue instanceof Integer)) {
-            throw newCfgFileException(
-                    cfgFile, settingName,
-                    "Should be an integer number (32 bits max), but it's: "
-                    + settingValue);
-        }
-        return ((Integer) settingValue).intValue();
-    }
-
-    /* Unused at the moment
-    @SuppressWarnings("unchecked")
-    private List<String> castSettingToListOfStrings(File cfgFile,
-            String settingName, Object settingValue) throws DocgenException {
-        if (!(settingValue instanceof List)) {
-            throw newCfgFileException(
-                    cfgFile, settingName,
-                    "Should be a list, but it's a "
-                    + CJSONInterpreter.cjsonTypeOf(settingValue) + ".");
-        }
-        List ls = (List) settingValue;
-
-        for (Object i : ls) {
-            if (!(i instanceof String)) {
-            throw newCfgFileException(
-                    cfgFile, settingName,
-                    "Should be a list of strings, but one if the list items "
-                    + "is a " + CJSONInterpreter.cjsonTypeOf(i) + ".");
-            }
-        }
-
-        return ls;
-    }
-    */
-
-    private String castSettingValueMapValueToString(File cfgFile,
-            String settingName, Object mapEntryValue) throws DocgenException {
-        if (mapEntryValue != null && !(mapEntryValue instanceof String)) {
-            throw newCfgFileException(cfgFile, settingName,
-                    "The values in the key-value pairs of this map must be "
-                    + "strings, but some of them is a "
-                    + CJSONInterpreter.cjsonTypeOf(mapEntryValue) + ".");
-        }
-        return (String) mapEntryValue;
-    }
-
-    @SuppressWarnings("unchecked")
-    private Map<String, String> castSettingValueMapValueToMapOfStringString(File cfgFile,
-            String settingName, Object mapEntryValue, Set<String> requiredKeys, Set<String> optionalKeys)
-            throws DocgenException {
-        if (!(mapEntryValue instanceof Map)) {
-            throw newCfgFileException(cfgFile, settingName,
-                    "The values in the key-value pairs of this map must be "
-                    + "Map-s, but some of them is a "
-                    + CJSONInterpreter.cjsonTypeOf(mapEntryValue) + ".");
-        }
-
-        if (requiredKeys == null) requiredKeys = Collections.emptySet();
-        if (optionalKeys == null) optionalKeys = Collections.emptySet();
-
-        Map<?, ?> mapEntryValueAsMap = (Map<?, ?>) mapEntryValue;
-        for (Entry<?, ?> valueEnt : mapEntryValueAsMap.entrySet()) {
-            Object key = valueEnt.getKey();
-            if (!(key instanceof String)) {
-                throw newCfgFileException(cfgFile, settingName,
-                        "The values in the key-value pairs of this map must be "
-                        + "Map<String, String>-s, but some of the keys is a "
-                        + CJSONInterpreter.cjsonTypeOf(mapEntryValue) + ".");
-            }
-            if (!(valueEnt.getValue() instanceof String)) {
-                throw newCfgFileException(cfgFile, settingName,
-                        "The values in the key-value pairs of this map must be "
-                                + "Map<String, String>-s, but some of the values is a "
-                        + CJSONInterpreter.cjsonTypeOf(valueEnt.getValue()) + ".");
-            }
-            if (!requiredKeys.contains(key) && !optionalKeys.contains(key)) {
-                StringBuilder sb = new StringBuilder();
-                sb.append("Unsupported key: ");
-                sb.append(StringUtil.jQuote(key));
-                sb.append(". Supported keys are: ");
-                boolean isFirst = true;
-                for (String supportedKey : requiredKeys) {
-                    if (!isFirst) {
-                        sb.append(", ");
-                    } else {
-                        isFirst = false;
-                    }
-                    sb.append(StringUtil.jQuote(supportedKey));
-                }
-                for (String supportedKey : optionalKeys) {
-                    if (!isFirst) {
-                        sb.append(", ");
-                    } else {
-                        isFirst = false;
-                    }
-                    sb.append(StringUtil.jQuote(supportedKey));
-                }
-                throw newCfgFileException(cfgFile, settingName, sb.toString());
-            }
-        }
-        for (String requiredKey : requiredKeys) {
-            if (!mapEntryValueAsMap.containsKey(requiredKey)) {
-                throw newCfgFileException(cfgFile, settingName,
-                        "Missing map key from nested Map: " + requiredKey);
-            }
-        }
-        return (Map<String, String>) mapEntryValue;
-    }
-
-    private Logo castMapToLogo(File cfgFile, final String settingName, Map<String, Object> map)
-            throws DocgenException {
-        Logo logo = new Logo();
-        for (Entry<String, Object> ent : map.entrySet()) {
-            String key = ent.getKey();
-            String value = castSettingValueMapValueToString(cfgFile, settingName, ent.getValue());
-            switch (key) {
-            case SETTING_LOGO_KEY_SRC:
-                logo.setSrc(value);
-                break;
-            case SETTING_LOGO_KEY_ALT:
-                logo.setAlt(value);
-                break;
-            case SETTING_LOGO_KEY_HREF:
-                logo.setHref(value);
-                break;
-            default:
-                throw newCfgFileException(cfgFile, SETTING_LOGO, "Unknown logo option: " + StringUtil.jQuote(key));
-            }
-        }
-
-        if (logo.getSrc() == null) {
-            throw newCfgFileException(cfgFile, SETTING_LOGO, "Missing logo option: " + SETTING_LOGO_KEY_SRC);
-        }
-        if (logo.getAlt() == null) {
-            throw newCfgFileException(cfgFile, SETTING_LOGO, "Missing logo option: " + SETTING_LOGO_KEY_ALT);
-        }
-        if (logo.getHref() == null) {
-            throw newCfgFileException(cfgFile, SETTING_LOGO, "Missing logo option: " + SETTING_LOGO_KEY_HREF);
-        }
-
-        return logo;
-    }
-
-    private String getFileContentForSetting(File cfgFile,
-            String settingName, Object settingValue) throws DocgenException {
-        String settingValueStr = castSettingToString(cfgFile, settingName, settingValue);
+    private String getFileContentForSetting(SettingName settingName, Object settingValue) {
+        String settingValueStr = castSetting(settingName, settingValue, String.class);
         File f = new File(getSourceDirectory(), settingValueStr);
         if (!f.exists()) {
             throw newCfgFileException(
-                    cfgFile, settingName,
+                    settingName,
                     "File not found: " + f.toPath());
         }
         try {
             return FileUtil.loadString(f, UTF_8);
         } catch (IOException e) {
             throw newCfgFileException(
-                    cfgFile, "Error while reading file for setting \"" + settingName + "\": " + f.toPath(),
+                    settingName,
+                    "Error while reading file: " + f.toPath(),
                     e);
         }
     }
 
-    private void copyCommonStatic(String staticFileName) throws IOException, DocgenException {
+    private void copyCommonStatic(String staticFileName) throws IOException {
         String resourcePath = "statics/" + staticFileName;
         try (InputStream in = Transform.class.getResourceAsStream(resourcePath)) {
             if (in == null) {
@@ -1737,7 +1411,7 @@
         preprocessDOM_misc_inner(doc,
                 new PreprocessDOMMisc_GlobalState(),
                 new PreprocessDOMMisc_ParentSectState());
-        indexEntries = new ArrayList<String>(primaryIndexTermLookup.keySet());
+        indexEntries = new ArrayList<>(primaryIndexTermLookup.keySet());
         Collections.sort(indexEntries, Collator.getInstance(locale));
     }
 
@@ -1898,10 +1572,10 @@
         }
     }
 
-    private void preprocessDOM_applyRemoveNodesWhenOnlineSetting(Document doc) throws DocgenException {
+    private void preprocessDOM_applyRemoveNodesWhenOnlineSetting(Document doc) {
         if (offline || removeNodesWhenOnline == null || removeNodesWhenOnline.isEmpty()) return;
 
-        HashSet<String> idsToRemoveLeft = new HashSet<String>(removeNodesWhenOnline);
+        HashSet<String> idsToRemoveLeft = new HashSet<>(removeNodesWhenOnline);
         preprocessDOM_applyRemoveNodesWhenOnlineSetting_inner(
                 doc.getDocumentElement(), idsToRemoveLeft);
         if (!idsToRemoveLeft.isEmpty()) {
@@ -1936,8 +1610,7 @@
      * Annotates the document structure nodes with so called ranks.
      * About ranks see: {@link #setting_lowestFileElementRank}.
      */
-    private void preprocessDOM_addRanks(Document doc)
-            throws DocgenException {
+    private void preprocessDOM_addRanks(Document doc) {
         Element root = doc.getDocumentElement();
         String rootName = root.getLocalName();
         if (rootName.equals(E_BOOK)) {
@@ -1954,9 +1627,7 @@
         }
     }
 
-    private void preprocessDOM_addRanks_underBookRank(
-            Element root) throws DocgenException {
-
+    private void preprocessDOM_addRanks_underBookRank(Element root) {
         // Find the common rank:
         DocumentStructureRank commonRank = null;
         for (Element child : XMLUtil.childrenElementsOf(root)) {
@@ -2003,8 +1674,7 @@
         }
     }
 
-    private void preprocessDOM_addRanks_underTruePart(
-            Node parent) throws DocgenException {
+    private void preprocessDOM_addRanks_underTruePart(Node parent) {
         for (Element child : XMLUtil.childrenElementsOf(parent)) {
             if (DOCUMENT_STRUCTURE_ELEMENTS.contains(child.getLocalName())) {
                 child.setAttribute(
@@ -2016,7 +1686,7 @@
     }
 
     private void preprocessDOM_addRanks_underChapterRankOrDeeper(
-            Element parent, int underSectionRank) throws DocgenException {
+            Element parent, int underSectionRank) {
         for (Element child : XMLUtil.childrenElementsOf(parent)) {
             if (DOCUMENT_STRUCTURE_ELEMENTS.contains(child.getLocalName())) {
                 if (child.getLocalName().equals(E_SIMPLESECT)) {
@@ -2047,7 +1717,7 @@
         }
     }
 
-    private void preprocessDOM_buildTOC(Document doc) throws DocgenException {
+    private void preprocessDOM_buildTOC(Document doc) {
         preprocessDOM_buildTOC_inner(doc, 0, null);
         if (tocNodes.size() > 0) {
             preprocessDOM_buildTOC_checkEnsureHasIndexHhml(tocNodes);
@@ -2092,8 +1762,7 @@
               + "\" setting. Maybe it's incompatible with the structure of "
               + "this document.)";
 
-    private void preprocessDOM_buildTOC_checkTOCTopology(TOCNode tocNode)
-    throws DocgenException {
+    private void preprocessDOM_buildTOC_checkTOCTopology(TOCNode tocNode) {
         // Check parent-child relation:
         TOCNode parent = tocNode.getParent();
         if (parent != null && !parent.getElement().isSameNode(
@@ -2190,8 +1859,7 @@
               + "\" setting. Maybe it's incompatible with the structure of "
               + "this document.)";
 
-    private void preprocessDOM_buildTOC_checkFileTopology(TOCNode tocNode)
-            throws DocgenException {
+    private void preprocessDOM_buildTOC_checkFileTopology(TOCNode tocNode) {
         TOCNode firstChild  = tocNode.getFirstChild();
         if (firstChild != null) {
             boolean firstIsFileElement = firstChild.isFileElement();
@@ -2229,8 +1897,7 @@
     }
 
     private TOCNode preprocessDOM_buildTOC_inner(Node node,
-            final int sectionLevel, TOCNode parentTOCNode)
-            throws DocgenException {
+            final int sectionLevel, TOCNode parentTOCNode) {
         TOCNode curTOCNode = null;
         int newSectionLevel = sectionLevel;
 
@@ -2316,7 +1983,7 @@
         return curTOCNode;
     }
 
-    private String getExternalLinkTOCNodeURLOrNull(Element elem) throws DocgenException {
+    private String getExternalLinkTOCNodeURLOrNull(Element elem) {
         if (elem.getParentNode() instanceof Document) {
             // The document element is never an external link ToC node.
             return null;
@@ -2365,7 +2032,7 @@
      * @param tocNodes
      * @throws DocgenException
      */
-    private void preprocessDOM_buildTOC_checkEnsureHasIndexHhml(List<TOCNode> tocNodes) throws DocgenException {
+    private void preprocessDOM_buildTOC_checkEnsureHasIndexHhml(List<TOCNode> tocNodes) {
         for (TOCNode tocNode : tocNodes) {
             if (tocNode.getOutputFileName() != null && tocNode.getOutputFileName().equals(FILE_INDEX_HTML)) {
                 return;
@@ -2468,13 +2135,13 @@
 
         String primaryText = primary.getFirstChild().getNodeValue().trim();
         if (!primaryIndexTermLookup.containsKey(primaryText)) {
-            primaryIndexTermLookup.put(primaryText, new ArrayList<NodeModel>());
+            primaryIndexTermLookup.put(primaryText, new ArrayList<>());
         }
 
         if (secondary != null) {
             if (!secondaryIndexTermLookup.containsKey(primaryText)) {
                 secondaryIndexTermLookup.put(
-                        primaryText, new TreeMap<String, List<NodeModel>>());
+                        primaryText, new TreeMap<>());
             }
             Map<String, List<NodeModel>> m = secondaryIndexTermLookup.get(
                     primaryText);
@@ -2482,7 +2149,7 @@
                     .trim();
             List<NodeModel> nodes = m.get(secondaryText);
             if (nodes == null) {
-                nodes = new ArrayList<NodeModel>();
+                nodes = new ArrayList<>();
                 m.put(secondaryText, nodes);
             }
             nodes.add(NodeModel.wrap(node));
@@ -2670,8 +2337,7 @@
         return false;
     }
 
-    private String createElementLinkURL(final Element elem)
-            throws DocgenException {
+    private String createElementLinkURL(final Element elem) {
         if (elem.hasAttribute(A_DOCGEN_NOT_ADDRESSABLE)) {
             return null;
         }
@@ -2747,6 +2413,7 @@
 
     private TemplateMethodModelEx createLinkFromID = new TemplateMethodModelEx() {
 
+        @Override
         public Object exec(@SuppressWarnings("rawtypes") final List args)
                 throws TemplateModelException {
             if (args.size() != 1) {
@@ -2765,7 +2432,7 @@
 
     };
 
-    private String createLinkFromId(String id) throws DocgenException {
+    private String createLinkFromId(String id) {
         if (elementsById == null) {
             throw new IllegalStateException("Can't resolve ID as elementsById is still null: " + id);
         }
@@ -2781,6 +2448,7 @@
     private TemplateMethodModelEx createLinkFromNode
             = new TemplateMethodModelEx() {
 
+        @Override
         public Object exec(@SuppressWarnings("rawtypes") final List args)
                 throws TemplateModelException {
 
@@ -2817,6 +2485,7 @@
 
     private TemplateMethodModelEx nodeFromID = new TemplateMethodModelEx() {
 
+        @Override
         public Object exec(@SuppressWarnings("rawtypes") List args)
                 throws TemplateModelException {
             Node node = elementsById.get(getArgString(args, 0));
diff --git a/freemarker-docgen-core/src/main/resources/org/freemarker/docgen/core/templates/footer.ftlh b/freemarker-docgen-core/src/main/resources/org/freemarker/docgen/core/templates/footer.ftlh
index 92ba2f0..94cf04e 100644
--- a/freemarker-docgen-core/src/main/resources/org/freemarker/docgen/core/templates/footer.ftlh
+++ b/freemarker-docgen-core/src/main/resources/org/freemarker/docgen/core/templates/footer.ftlh
@@ -25,15 +25,15 @@
     <div class="site-footer"><#t>
       <#-- keep site-width inside so background extends -->
       <div class="site-width"><#t>
-        <#if footerSiteMap?? || socialLinks?? || showXXELogo>
+        <#if footerSiteMap?hasContent || socialLinks?hasContent || showXXELogo>
           <div class="footer-top"><#t>
             <div class="col-left sitemap"><#t>
-              <#if footerSiteMap??>
+              <#if footerSiteMap?hasContent>
                 <@siteMap columns=footerSiteMap /><#t>
               </#if>
             </div><#t>
             <div class="col-right"><#t>
-              <#if socialLinks??>
+              <#if socialLinks?hasContent>
                 <@social links=socialLinks />
               </#if>
               <#if showXXELogo>
diff --git a/freemarker-docgen-core/src/main/resources/org/freemarker/docgen/core/templates/header.ftlh b/freemarker-docgen-core/src/main/resources/org/freemarker/docgen/core/templates/header.ftlh
index 1e8993b..ad5e96b 100644
--- a/freemarker-docgen-core/src/main/resources/org/freemarker/docgen/core/templates/header.ftlh
+++ b/freemarker-docgen-core/src/main/resources/org/freemarker/docgen/core/templates/header.ftlh
@@ -22,7 +22,7 @@
 <#import "google.ftlh" as google>
 
 <#macro header>
-  <#if logo?? || tabs?? || secondaryTabs??>
+  <#if logo?? || tabs?hasContent || secondaryTabs?hasContent>
     <div class="header-top-bg"><#t>
       <div class="site-width header-top"><#t>
         <div id="hamburger-menu" role="button"></div><#t>
@@ -34,7 +34,7 @@
           </div>
         </#if>
         <@nav.tabs /><#t>
-        <#if secondaryTabs??>
+        <#if secondaryTabs?hasContent>
           <ul class="secondary-tabs"><#t>
             <#list secondaryTabs as tabTitle, tab>
               <li><#t>
diff --git a/freemarker-docgen-core/src/main/resources/org/freemarker/docgen/core/templates/node-handlers.ftlh b/freemarker-docgen-core/src/main/resources/org/freemarker/docgen/core/templates/node-handlers.ftlh
index 601af62..3be163b 100644
--- a/freemarker-docgen-core/src/main/resources/org/freemarker/docgen/core/templates/node-handlers.ftlh
+++ b/freemarker-docgen-core/src/main/resources/org/freemarker/docgen/core/templates/node-handlers.ftlh
@@ -37,7 +37,7 @@
 
 <#macro @element>
   <#stop "This DocBook element is not supported by the Docgen transformer, "
-      + "or wasn't expected where it occured: "
+      + "or wasn't expected where it occurred: "
       + .node?nodeName>
 </#macro>
 
diff --git a/freemarker-docgen-core/src/test/java/org/freemarker/docgen/core/SettingNameTest.java b/freemarker-docgen-core/src/test/java/org/freemarker/docgen/core/SettingNameTest.java
new file mode 100644
index 0000000..a0eab82
--- /dev/null
+++ b/freemarker-docgen-core/src/test/java/org/freemarker/docgen/core/SettingNameTest.java
@@ -0,0 +1,32 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.freemarker.docgen.core;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import org.junit.jupiter.api.Test;
+
+public class SettingNameTest {
+    @Test
+    public void toStringTest() {
+        assertEquals("a", SettingName.topLevel(null, "a").toString());
+        assertEquals("a.b[1]", SettingName.topLevel(null, "a").subKey("b").subKey(1).toString());
+    }
+}
diff --git a/freemarker-docgen-core/src/test/java/org/freemarker/docgen/core/SettingUtilsTest.java b/freemarker-docgen-core/src/test/java/org/freemarker/docgen/core/SettingUtilsTest.java
new file mode 100644
index 0000000..b615f3b
--- /dev/null
+++ b/freemarker-docgen-core/src/test/java/org/freemarker/docgen/core/SettingUtilsTest.java
@@ -0,0 +1,152 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.freemarker.docgen.core;
+
+import static org.freemarker.docgen.core.SettingUtils.*;
+import static org.hamcrest.MatcherAssert.*;
+import static org.hamcrest.Matchers.*;
+import static org.junit.jupiter.api.Assertions.*;
+
+import java.util.List;
+import java.util.Map;
+
+import org.hamcrest.Matchers;
+import org.junit.jupiter.api.Test;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
+
+public class SettingUtilsTest {
+
+    private static final SettingName SETTING_NAME = SettingName.topLevel(null, "foo");
+
+    @Test
+    public void testBasics() {
+        {
+            Object originalValue = 1;
+            Object value = castSetting(SettingName.topLevel(null, "a"), originalValue, Integer.class);
+            assertEquals(originalValue, value);
+        }
+        {
+            ImmutableList<ImmutableList<?>> originalValue = ImmutableList.of(ImmutableList.of(), ImmutableList.of(1));
+            Object value = castSetting(
+                    SETTING_NAME,
+                    originalValue, true,
+                    List.class,
+                    new ListItemType(List.class),
+                    new ListItemType(Integer.class)
+            );
+            assertEquals(originalValue, value);
+        }
+        {
+            ImmutableMap<String, ImmutableMap<?, ?>> originalValue = ImmutableMap.of(
+                    "x", ImmutableMap.of(),
+                    "y", ImmutableMap.of("u", 1));
+            Object value = castSetting(
+                    SETTING_NAME,
+                    originalValue,
+                    Map.class,
+                    new MapEntryType<>(String.class, Map.class),
+                    new MapEntryType(String.class, Integer.class)
+            );
+            assertEquals(originalValue, value);
+        }
+    }
+
+    @Test
+    public void testOptional() {
+        assertNull(castSetting(SETTING_NAME, null, true, Integer.class));
+        try {
+            castSetting(SETTING_NAME, null, Integer.class);
+            fail();
+        } catch (DocgenException e) {
+            assertThat(
+                    e.getMessage(),
+                    allOf(containsString("required"), containsString(SETTING_NAME.toString())));
+        }
+    }
+
+    @Test
+    public void testMapKeyValidation() {
+        ImmutableSet<String> requiredKeys = ImmutableSet.of("reqKey1", "reqKey2");
+        ImmutableSet<String> optionalKeys = ImmutableSet.of("optKey");
+        {
+            ImmutableMap<String, Integer> originalValue = ImmutableMap.of(
+                    "reqKey1", 1,
+                    "reqKey2", 2,
+                    "optKey", 3);
+            Object value = castSetting(
+                    SETTING_NAME, originalValue,
+                    Map.class,
+                    new MapEntryType(
+                            String.class, requiredKeys, optionalKeys,
+                            Integer.class));
+            assertEquals(originalValue, value);
+        }
+        {
+            ImmutableMap<String, Integer> originalValue = ImmutableMap.of(
+                    "reqKey1", 1,
+                    "reqKey2", 2);
+            Object value = castSetting(
+                    SETTING_NAME,
+                    originalValue,
+                    Map.class,
+                    new MapEntryType(
+                            String.class, requiredKeys, optionalKeys,
+                            Integer.class));
+            assertEquals(originalValue, value);
+        }
+        try {
+            castSetting(
+                    SETTING_NAME,
+                    ImmutableMap.of(
+                            "reqKey1", 1,
+                            "optKey", 3),
+                    Map.class,
+                    new MapEntryType(
+                            String.class, requiredKeys, optionalKeys,
+                            Integer.class));
+            fail();
+        } catch (DocgenException e) {
+            assertThat(
+                    e.getMessage(),
+                    allOf(containsString("reqKey2"), containsString(SETTING_NAME.toString())));
+        }
+        try {
+            castSetting(
+                    SETTING_NAME,
+                    ImmutableMap.of(
+                            "reqKey1", 1,
+                            "reqKey2", 2,
+                            "wrongKey", 2),
+                    Map.class,
+                    new MapEntryType(
+                            String.class, requiredKeys, optionalKeys,
+                            Integer.class));
+            fail();
+        } catch (DocgenException e) {
+            assertThat(
+                    e.getMessage(),
+                    allOf(containsString("wrongKey"), containsString(SETTING_NAME.toString())));
+        }
+    }
+
+}