docs: public boolean preValidateLevel(final Level level, final Map<String, SpecTopic> specTopics, final Map<String, InfoTopic> infoTopics,
              final Set<String> processedFixedUrls, final BookType bookType, final ContentSpec contentSpec) {
          // Check if the app should be shutdown
          if (isShuttingDown.get()) {
              shutdown.set(true);
              return false;
          }
  
          boolean valid = true;
  
          // Make sure the level has a type, if it doesn't then return false immediately
          final LevelType levelType = level.getLevelType();
          if (levelType == null) {
              log.error(ProcessorConstants.ERROR_PROCESSING_ERROR_MSG);
              return false;
          }
  
          // Check that the level isn't empty
          if (levelType != LevelType.PART && level.getNumberOfSpecTopics() <= 0 && level.getNumberOfChildLevels() <= 0
                  && level.getNumberOfCommonContents() <= 0) {
              log.error(format(ProcessorConstants.ERROR_LEVEL_NO_TOPICS_MSG, level.getLineNumber(), levelType.getTitle(),
                      levelType.getTitle(), level.getText()));
              valid = false;
          } else if (levelType == LevelType.PART && level.getNumberOfChildLevels() <= 0 && level.getNumberOfCommonContents() <= 0) {
              log.error(format(ProcessorConstants.ERROR_LEVEL_NO_CHILD_LEVELS_MSG, level.getLineNumber(), levelType.getTitle(),
                      levelType.getTitle(), level.getText()));
              valid = false;
          }
  
          // Sections have to have more than just one initial text topic
          if (levelType == LevelType.SECTION && level.getNumberOfSpecTopics() <= 0 && level.getNumberOfCommonContents() <= 0
                  && level.getNumberOfChildLevels() <= 1 && level.getFirstSpecNode() instanceof InitialContent) {
              final InitialContent initialContent = ((InitialContent) level.getFirstSpecNode());
              if ((initialContent.getNumberOfSpecTopics() + initialContent.getNumberOfCommonContents()) <= 1) {
                  log.error(format(ProcessorConstants.ERROR_SECTION_NO_TOPICS_OR_INITIAL_CONTENT_MSG, level.getLineNumber(), level.getText()));
                  valid = false;
              }
          }
  
          // Check that a title has been specified
          if (isNullOrEmpty(level.getTitle())) {
              log.error(String.format(ProcessorConstants.ERROR_LEVEL_NO_TITLE_MSG, level.getLineNumber(), levelType.getTitle(),
                      level.getText()));
              valid = false;
          }
  
  
          // Check that the initial content level doesn't have a fixed url and if it does then print a warning,
          // otherwise validate the fixed url
          if (level.getLevelType() == LevelType.INITIAL_CONTENT && !isNullOrEmpty(level.getFixedUrl())) {
              log.warn(format(ProcessorConstants.WARN_FIXED_URL_WILL_BE_IGNORED_MSG, level.getLineNumber(), level.getText()));
              level.setFixedUrl(null);
          } else if (!isNullOrEmpty(level.getFixedUrl())) {
              if (!validateFixedUrl(level, processedFixedUrls)) {
                  valid = false;
              } else {
                  processedFixedUrls.add(level.getFixedUrl());
              }
          }
  
          // Validate the info topic
          if (level.getInfoTopic() != null) {
              preValidateInfoTopic(level.getInfoTopic(), contentSpec, infoTopics);
          }
  
          // Validate the sub levels and topics
          for (final Node childNode : level.getChildNodes()) {
              if (childNode instanceof Level) {
                  if (!preValidateLevel((Level) childNode, specTopics, infoTopics, processedFixedUrls, bookType, contentSpec)) {
                      valid = false;
                  }
              } else if (childNode instanceof CommonContent) {
                  if (!preValidateCommonContent((CommonContent) childNode, bookType)) {
                      valid = false;
                  }
              } else if (childNode instanceof SpecTopic) {
                  if (!preValidateTopic((SpecTopic) childNode, specTopics, processedFixedUrls, bookType, contentSpec)) {
                      valid = false;
                  }
              }
          }
  
          // Validate certain requirements depending on the type of level
          LevelType parentLevelType = level.getParent() != null ? level.getParent().getLevelType() : null;
          if (bookType == BookType.ARTICLE || bookType == BookType.ARTICLE_DRAFT) {
              switch (levelType) {
                  case APPENDIX:
                      if (parentLevelType != LevelType.BASE) {
                          log.error(String.format(ProcessorConstants.ERROR_ARTICLE_NESTED_APPENDIX_MSG, level.getLineNumber(),
                                  level.getText()));
                          valid = false;
                      }
  
                      // Check that the appendix is at the end of the article
                      final Integer nodeListId = level.getParent().getChildNodes().indexOf(level);
                      final ListIterator<Node> parentNodes = level.getParent().getChildNodes().listIterator(nodeListId);
  
                      while (parentNodes.hasNext()) {
                          final Node node = parentNodes.next();
                          if (node instanceof Level && ((Level) node).getLevelType() != LevelType.APPENDIX) {
                              log.error(format(ProcessorConstants.ERROR_CS_APPENDIX_STRUCTURE_MSG, level.getLineNumber(), level.getText()));
                              valid = false;
                          }
                      }
                      break;
                  case CHAPTER:
                      log.error(String.format(ProcessorConstants.ERROR_ARTICLE_CHAPTER_MSG, level.getLineNumber(), level.getText()));
                      valid = false;
                      break;
                  case PROCESS:
                      log.error(String.format(ProcessorConstants.ERROR_ARTICLE_PROCESS_MSG, level.getLineNumber(), level.getText()));
                      valid = false;
                      break;
                  case PART:
                      log.error(String.format(ProcessorConstants.ERROR_ARTICLE_PART_MSG, level.getLineNumber(), level.getText()));
                      valid = false;
                      break;
                  case PREFACE:
                      log.error(format(ProcessorConstants.ERROR_ARTICLE_PREFACE_MSG, level.getLineNumber(), level.getText()));
                      valid = false;
                      break;
                  case SECTION:
                      if (!(parentLevelType == LevelType.BASE || parentLevelType == LevelType.SECTION)) {
                          log.error(format(ProcessorConstants.ERROR_ARTICLE_SECTION_MSG, level.getLineNumber(), level.getText()));
                          valid = false;
                      }
                      break;
                  case INITIAL_CONTENT:
                      // Check that the initial content is at the start of the container
                      if (level.getParent() == null || level.getParent().getFirstSpecNode() != level) {
                          log.error(format(ProcessorConstants.ERROR_CS_INITIAL_CONTENT_STRUCTURE_MSG, level.getLineNumber(),
                                  parentLevelType.getTitle(), level.getText()));
                          valid = false;
                      }
                      break;
                  default:
                      break;
              }
          }
          // Generic book based validation
          else {
              switch (levelType) {
                  case APPENDIX:
                      if (!(parentLevelType == LevelType.BASE || parentLevelType == LevelType.PART)) {
                          log.error(format(ProcessorConstants.ERROR_CS_NESTED_APPENDIX_MSG, level.getLineNumber(), level.getText()));
                          valid = false;
                      }
  
                      // Check that the appendix is at the end of the book
                      final Integer nodeListId = level.getParent().getChildNodes().indexOf(level);
                      final ListIterator<Node> parentNodes = level.getParent().getChildNodes().listIterator(nodeListId);
  
                      while (parentNodes.hasNext()) {
                          final Node node = parentNodes.next();
                          if (node instanceof Level && ((Level) node).getLevelType() != LevelType.APPENDIX) {
                              log.error(format(ProcessorConstants.ERROR_CS_APPENDIX_STRUCTURE_MSG, level.getLineNumber(), level.getText()));
                              valid = false;
                          }
                      }
  
                      break;
                  case CHAPTER:
                      if (!(parentLevelType == LevelType.BASE || parentLevelType == LevelType.PART)) {
                          log.error(format(ProcessorConstants.ERROR_CS_NESTED_CHAPTER_MSG, level.getLineNumber(), level.getText()));
                          valid = false;
                      }
                      break;
                  case PROCESS:
                      // Check that the process has no children
                      if (level.getNumberOfChildLevels() != 0) {
                          log.error(format(ProcessorConstants.ERROR_PROCESS_HAS_LEVELS_MSG, level.getLineNumber(), level.getText()));
                          valid = false;
                      }
                      break;
                  case PART:
                      if (parentLevelType != LevelType.BASE) {
                          log.error(format(ProcessorConstants.ERROR_CS_NESTED_PART_MSG, level.getLineNumber(), level.getText()));
                          valid = false;
                      }
                      break;
                  case PREFACE:
                      if (parentLevelType != LevelType.BASE || parentLevelType == LevelType.PART) {
                          log.error(format(ProcessorConstants.ERROR_CS_NESTED_PREFACE_MSG, level.getLineNumber(), level.getText()));
                          valid = false;
                      }
                      break;
                  case SECTION:
                      if (!(parentLevelType == LevelType.APPENDIX || parentLevelType == LevelType.CHAPTER || parentLevelType == LevelType
                              .PREFACE || parentLevelType == LevelType.SECTION)) {
                          log.error(format(ProcessorConstants.ERROR_CS_SECTION_NO_CHAPTER_MSG, level.getLineNumber(), level.getText()));
                          valid = false;
                      }
                      break;
                  case INITIAL_CONTENT:
                      // Check that the initial content is at the start of the container
                      if (level.getParent() == null || level.getParent().getFirstSpecNode() != level) {
                          log.error(format(ProcessorConstants.ERROR_CS_INITIAL_CONTENT_STRUCTURE_MSG, level.getLineNumber(),
                                  parentLevelType.getTitle(), level.getText()));
                          valid = false;
                      }
                      break;
                  default:
                      break;
              }
          }
  
          // Validate the tags
          if (processingOptions.isPrintChangeWarnings() && !level.getTags(false).isEmpty() && level.hasRevisionSpecTopics()) {
              log.warn(String.format(ProcessorConstants.WARN_LEVEL_TAGS_IGNORE_MSG, level.getLineNumber(), level.getLevelType().getTitle(),
                      "revision", level.getText()));
          }
  
          // Check for conflicting conditions
          checkForConflictingCondition(level, contentSpec);
  
          return valid;
      }