docs: public boolean preValidateContentSpec(final ContentSpec contentSpec) {
          // Create the map of unique ids to spec topics
          final Map<String, SpecTopic> specTopicMap = ContentSpecUtilities.getUniqueIdSpecTopicMap(contentSpec);
          final Map<String, InfoTopic> infoTopicMap = ContentSpecUtilities.getUniqueIdInfoTopicMap(contentSpec);
  
          // Get all server defined fixed urls
          final Set<String> processedFixedUrls = new HashSet<String>();
  
          // Check if the app should be shutdown
          if (isShuttingDown.get()) {
              shutdown.set(true);
              return false;
          }
  
          boolean valid = true;
          if (isNullOrEmpty(contentSpec.getTitle())) {
              log.error(ProcessorConstants.ERROR_CS_NO_TITLE_MSG);
              valid = false;
          }
  
          if (isNullOrEmpty(contentSpec.getProduct())) {
              log.error(ProcessorConstants.ERROR_CS_NO_PRODUCT_MSG);
              valid = false;
          }
  
          if (!isNullOrEmpty(contentSpec.getVersion()) && !contentSpec.getVersion().matches(
                  ProcessorConstants.PRODUCT_VERSION_VALIDATE_REGEX)) {
              log.error(format(ProcessorConstants.ERROR_INVALID_VERSION_NUMBER_MSG, CommonConstants.CS_VERSION_TITLE));
              valid = false;
          }
  
          if (isNullOrEmpty(contentSpec.getFormat())) {
              log.error(ProcessorConstants.ERROR_CS_NO_DTD_MSG);
              valid = false;
              // Check that the DTD specified is a valid DTD format
          } else if (!contentSpec.getFormat().equalsIgnoreCase(CommonConstants.DOCBOOK_45_TITLE)
                  && !contentSpec.getFormat().equalsIgnoreCase(CommonConstants.DOCBOOK_50_TITLE)) {
              log.error(ProcessorConstants.ERROR_CS_INVALID_DTD_MSG);
              valid = false;
          }
  
          if (isNullOrEmpty(contentSpec.getCopyrightHolder())) {
              log.error(ProcessorConstants.ERROR_CS_NO_COPYRIGHT_MSG);
              valid = false;
          }
  
          // Check that the book type is valid
          if (contentSpec.getBookType() == BookType.INVALID) {
              log.error(ProcessorConstants.ERROR_INVALID_BOOK_TYPE_MSG);
              valid = false;
          }
  
          // Check that the Copyright year is valid
          if (contentSpec.getCopyrightYear() != null && !contentSpec.getCopyrightYear().matches(
                  ProcessorConstants.COPYRIGHT_YEAR_VALIDATE_REGEX)) {
              log.error(ProcessorConstants.ERROR_INVALID_CS_COPYRIGHT_YEAR_MSG);
              valid = false;
          }
  
          // Check the version variables are all valid
          if (contentSpec.getBookVersion() != null && !contentSpec.getBookVersion().matches(ProcessorConstants.VERSION_VALIDATE_REGEX)) {
              log.error(format(ProcessorConstants.ERROR_INVALID_VERSION_NUMBER_MSG, CommonConstants.CS_BOOK_VERSION_TITLE));
              valid = false;
          }
  
          // Check the POM version is valid if it's set
          if (!isNullOrEmpty(contentSpec.getPOMVersion()) && !contentSpec.getPOMVersion().matches(
                  ProcessorConstants.PRODUCT_VERSION_VALIDATE_REGEX)) {
              log.error(format(ProcessorConstants.ERROR_INVALID_VERSION_NUMBER_MSG, CommonConstants.CS_MAVEN_POM_VERSION_TITLE));
              valid = false;
          }
  
          if (contentSpec.getEdition() != null && !contentSpec.getEdition().matches(ProcessorConstants.PRODUCT_VERSION_VALIDATE_REGEX)) {
              log.error(format(ProcessorConstants.ERROR_INVALID_VERSION_NUMBER_MSG, CommonConstants.CS_EDITION_TITLE));
              valid = false;
          }
  
          // Check for a negative pubsnumber
          if (contentSpec.getPubsNumber() != null && contentSpec.getPubsNumber() < 0) {
              log.error(ProcessorConstants.ERROR_INVALID_PUBSNUMBER_MSG);
              valid = false;
          }
  
          // Check that the default publican.cfg exists
          if (!contentSpec.getDefaultPublicanCfg().equals(CommonConstants.CS_PUBLICAN_CFG_TITLE)) {
              final String name = contentSpec.getDefaultPublicanCfg();
              final Matcher matcher = CSConstants.CUSTOM_PUBLICAN_CFG_PATTERN.matcher(name);
              final String fixedName = matcher.find() ? matcher.group(1) : name;
              if (contentSpec.getAdditionalPublicanCfg(fixedName) == null) {
                  log.error(String.format(ProcessorConstants.ERROR_INVALID_DEFAULT_PUBLICAN_CFG_MSG, name));
                  valid = false;
              }
          }
  
          // BZ#1091776 Check that mainfile isn't used
          if (doesPublicanCfgsContainValue(contentSpec, "mainfile")) {
              log.warn(ProcessorConstants.WARN_MAINFILE_WILL_BE_REMOVED_MSG);
          }
  
          // Check that any metadata topics are valid
          if (contentSpec.getRevisionHistory() != null && !preValidateTopic(contentSpec.getRevisionHistory(), specTopicMap,
                  processedFixedUrls, contentSpec.getBookType(), false, contentSpec)) {
              valid = false;
          }
          if (contentSpec.getLegalNotice() != null && !preValidateTopic(contentSpec.getLegalNotice(), specTopicMap, processedFixedUrls,
                  contentSpec.getBookType(),
                  false, contentSpec)) {
              valid = false;
          }
          if (contentSpec.getAuthorGroup() != null && !preValidateTopic(contentSpec.getAuthorGroup(), specTopicMap, processedFixedUrls,
                  contentSpec.getBookType(),
                  false, contentSpec)) {
              valid = false;
          }
          if (contentSpec.getAbstractTopic() != null && !preValidateTopic(contentSpec.getAbstractTopic(), specTopicMap, processedFixedUrls,
                  contentSpec.getBookType(), false, contentSpec)) {
              valid = false;
          }
  
          // Print Warnings for content that maybe important
          if (isNullOrEmpty(contentSpec.getSubtitle())) {
              log.warn(ProcessorConstants.WARN_CS_NO_SUBTITLE_MSG);
          }
          if (isNullOrEmpty(contentSpec.getAbstract()) && contentSpec.getAbstractTopic() == null) {
              log.warn(ProcessorConstants.WARN_CS_NO_ABSTRACT_MSG);
          } else if (!isNullOrEmpty(contentSpec.getAbstract())) {
              // Check to make sure the abstract is at least valid XML
              String wrappedAbstract = contentSpec.getAbstract();
              if (!contentSpec.getAbstract().matches("^<(formal|sim)?para>(.|\\s)*")) {
                  wrappedAbstract = "<para>" + contentSpec.getAbstract() + "</para>";
              }
              wrappedAbstract = "<abstract>" + wrappedAbstract + "</abstract>";
  
              if (!preValidateXML(contentSpec.getAbstractNode(), wrappedAbstract, contentSpec.getFormat())) {
                  valid = false;
              }
          }
  
          // Check to make sure all key value nodes a valid (that is they have a key and value specified
          for (final Node node : contentSpec.getNodes()) {
              if (node instanceof KeyValueNode) {
                  final KeyValueNode keyValueNode = (KeyValueNode) node;
                  if (isNullOrEmpty(keyValueNode.getKey())) {
                      valid = false;
                      log.error(format(ProcessorConstants.ERROR_INVALID_METADATA_FORMAT_MSG, keyValueNode.getLineNumber(),
                              keyValueNode.getText()));
                  } else {
                      final Object value = keyValueNode.getValue();
                      if (value instanceof String && isNullOrEmpty((String) value)) {
                          valid = false;
                          log.error(format(ProcessorConstants.ERROR_INVALID_METADATA_NO_VALUE_MSG, keyValueNode.getLineNumber(),
                                  keyValueNode.getText()));
                      } else if (value == null) {
                          valid = false;
                          log.error(format(ProcessorConstants.ERROR_INVALID_METADATA_NO_VALUE_MSG, keyValueNode.getLineNumber(),
                                  keyValueNode.getText()));
                      }
  
                      // Make sure the key is a valid meta data element
                      if (!ProcessorConstants.VALID_METADATA_KEYS.contains(
                              keyValueNode.getKey()) && !CSConstants.CUSTOM_PUBLICAN_CFG_PATTERN.matcher(keyValueNode.getKey()).matches()) {
                          valid = false;
                          log.error(format(ProcessorConstants.ERROR_UNRECOGNISED_METADATA_MSG, keyValueNode.getLineNumber(),
                                  keyValueNode.getText()));
                      }
  
                      // Make sure that the metadata is valid XML if it might be used
                      if (value instanceof String && !isNullOrEmpty((String) value)
                              && ProcessorConstants.METADATA_DOCBOOK_ELEMENTS.containsKey(keyValueNode.getKey())) {
                          final String element = ProcessorConstants.METADATA_DOCBOOK_ELEMENTS.get(keyValueNode.getKey());
                          final String wrappedElement = "<" + element + ">" + value + "</" + element + ">";
  
                          if (!preValidateXML(keyValueNode, wrappedElement, contentSpec.getFormat())) {
                              valid = false;
                          }
                      }
                  }
              }
          }
  
          // Content that should be checked when using the default preface
          if (contentSpec.getUseDefaultPreface()) {
              if (contentSpec.getFeedback() != null && !preValidateTopic(contentSpec.getFeedback(), specTopicMap, processedFixedUrls,
                      contentSpec.getBookType(),
                      false, contentSpec)) {
                  valid = false;
              }
          } else if (contentSpec.getFeedback() != null) {
              // Feedback cannot be used when the default preface is off, so log an error.
              log.error(format(ProcessorConstants.ERROR_FEEDBACK_USED_WITHOUT_DEFAULT_PREFACE_MSG, contentSpec.getFeedback().getText()));
              valid = false;
          }
  
          // Validate the custom entities
          if (!validateEntities(contentSpec)) {
              valid = false;
          }
  
          // Validate the basic bug link data
          if (!preValidateBugLinks(contentSpec)) {
              valid = false;
          }
  
          // Check that each level is valid
          if (!preValidateLevel(contentSpec.getBaseLevel(), specTopicMap, infoTopicMap, processedFixedUrls, contentSpec.getBookType(),
                  contentSpec)) {
              valid = false;
          }
  
          // Check that the relationships are valid
          if (!preValidateRelationships(contentSpec)) {
              valid = false;
          }
  
          /*
           * Ensure that no topics exist that have the same ID but different revisions. This needs to be done at the Content Spec
           * level rather than the Topic level as it isn't the topic that would be invalid but rather the set of topics
            * in the content specification.
           */
          if (!checkTopicsForInvalidDuplicates(contentSpec)) {
              valid = false;
          }
  
          return valid;
      }