Java tutorial
/* Copyright 2011-2014 Red Hat, Inc This file is part of PressGang CCMS. PressGang CCMS is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. PressGang CCMS is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with PressGang CCMS. If not, see <http://www.gnu.org/licenses/>. */ package org.jboss.pressgang.ccms.contentspec.builder; import static com.google.common.base.Strings.isNullOrEmpty; import java.io.BufferedReader; import java.io.ByteArrayInputStream; import java.io.File; import java.io.IOException; import java.io.InputStreamReader; import java.io.StringReader; import java.io.UnsupportedEncodingException; import java.text.DateFormat; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedHashMap; import java.util.LinkedList; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Map.Entry; import java.util.Properties; import java.util.ResourceBundle; import java.util.Set; import java.util.TreeSet; import java.util.concurrent.atomic.AtomicBoolean; import org.apache.commons.lang.StringEscapeUtils; import org.apache.log4j.Logger; import org.codehaus.jackson.io.JsonStringEncoder; import org.jboss.pressgang.ccms.contentspec.CommonContent; import org.jboss.pressgang.ccms.contentspec.ContentSpec; import org.jboss.pressgang.ccms.contentspec.ITopicNode; import org.jboss.pressgang.ccms.contentspec.InfoTopic; import org.jboss.pressgang.ccms.contentspec.InitialContent; import org.jboss.pressgang.ccms.contentspec.KeyValueNode; import org.jboss.pressgang.ccms.contentspec.Level; import org.jboss.pressgang.ccms.contentspec.SpecNode; import org.jboss.pressgang.ccms.contentspec.SpecTopic; import org.jboss.pressgang.ccms.contentspec.buglinks.BaseBugLinkStrategy; import org.jboss.pressgang.ccms.contentspec.buglinks.BugLinkOptions; import org.jboss.pressgang.ccms.contentspec.builder.constants.BuilderConstants; import org.jboss.pressgang.ccms.contentspec.builder.exception.BuildProcessingException; import org.jboss.pressgang.ccms.contentspec.builder.exception.BuilderCreationException; import org.jboss.pressgang.ccms.contentspec.builder.structures.BuildData; import org.jboss.pressgang.ccms.contentspec.builder.structures.DocBookBuildingOptions; import org.jboss.pressgang.ccms.contentspec.builder.structures.TopicErrorData; import org.jboss.pressgang.ccms.contentspec.builder.structures.TopicErrorDatabase.ErrorLevel; import org.jboss.pressgang.ccms.contentspec.builder.structures.TopicErrorDatabase.ErrorType; import org.jboss.pressgang.ccms.contentspec.builder.structures.TopicImageData; import org.jboss.pressgang.ccms.contentspec.builder.utils.DocBookBuildUtilities; import org.jboss.pressgang.ccms.contentspec.builder.utils.ReportUtilities; import org.jboss.pressgang.ccms.contentspec.constants.CSConstants; import org.jboss.pressgang.ccms.contentspec.entities.AuthorInformation; import org.jboss.pressgang.ccms.contentspec.enums.BookType; import org.jboss.pressgang.ccms.contentspec.enums.LevelType; import org.jboss.pressgang.ccms.contentspec.enums.TopicType; import org.jboss.pressgang.ccms.contentspec.exceptions.BugLinkException; import org.jboss.pressgang.ccms.contentspec.interfaces.ShutdownAbleApp; import org.jboss.pressgang.ccms.contentspec.sort.AuthorInformationComparator; import org.jboss.pressgang.ccms.contentspec.structures.XMLFormatProperties; import org.jboss.pressgang.ccms.contentspec.utils.ContentSpecUtilities; import org.jboss.pressgang.ccms.contentspec.utils.EntityUtilities; import org.jboss.pressgang.ccms.contentspec.utils.FixedURLGenerator; import org.jboss.pressgang.ccms.contentspec.utils.TranslationUtilities; import org.jboss.pressgang.ccms.provider.BlobConstantProvider; import org.jboss.pressgang.ccms.provider.CSNodeProvider; import org.jboss.pressgang.ccms.provider.ContentSpecProvider; import org.jboss.pressgang.ccms.provider.DataProviderFactory; import org.jboss.pressgang.ccms.provider.FileProvider; import org.jboss.pressgang.ccms.provider.ImageProvider; import org.jboss.pressgang.ccms.provider.PropertyTagProvider; import org.jboss.pressgang.ccms.provider.ServerSettingsProvider; import org.jboss.pressgang.ccms.provider.StringConstantProvider; import org.jboss.pressgang.ccms.provider.TagProvider; import org.jboss.pressgang.ccms.provider.TopicProvider; import org.jboss.pressgang.ccms.provider.TranslatedCSNodeProvider; import org.jboss.pressgang.ccms.provider.TranslatedContentSpecProvider; import org.jboss.pressgang.ccms.provider.TranslatedTopicProvider; import org.jboss.pressgang.ccms.provider.exception.NotFoundException; import org.jboss.pressgang.ccms.utils.common.CollectionUtilities; import org.jboss.pressgang.ccms.utils.common.DocBookUtilities; import org.jboss.pressgang.ccms.utils.common.ResourceUtilities; import org.jboss.pressgang.ccms.utils.common.StringUtilities; import org.jboss.pressgang.ccms.utils.common.XMLUtilities; import org.jboss.pressgang.ccms.utils.common.XMLValidator; import org.jboss.pressgang.ccms.utils.common.ZipUtilities; import org.jboss.pressgang.ccms.utils.constants.CommonConstants; import org.jboss.pressgang.ccms.utils.constants.CommonFilterConstants; import org.jboss.pressgang.ccms.utils.structures.DocBookVersion; import org.jboss.pressgang.ccms.utils.structures.InjectionError; import org.jboss.pressgang.ccms.utils.structures.Pair; import org.jboss.pressgang.ccms.wrapper.BlobConstantWrapper; import org.jboss.pressgang.ccms.wrapper.FileWrapper; import org.jboss.pressgang.ccms.wrapper.ImageWrapper; import org.jboss.pressgang.ccms.wrapper.LanguageFileWrapper; import org.jboss.pressgang.ccms.wrapper.LanguageImageWrapper; import org.jboss.pressgang.ccms.wrapper.LocaleWrapper; import org.jboss.pressgang.ccms.wrapper.PropertyTagInTopicWrapper; import org.jboss.pressgang.ccms.wrapper.ServerEntitiesWrapper; import org.jboss.pressgang.ccms.wrapper.StringConstantWrapper; import org.jboss.pressgang.ccms.wrapper.TagWrapper; import org.jboss.pressgang.ccms.wrapper.TopicWrapper; import org.jboss.pressgang.ccms.wrapper.TranslatedCSNodeStringWrapper; import org.jboss.pressgang.ccms.wrapper.TranslatedCSNodeWrapper; import org.jboss.pressgang.ccms.wrapper.TranslatedContentSpecWrapper; import org.jboss.pressgang.ccms.wrapper.TranslatedTopicWrapper; import org.jboss.pressgang.ccms.wrapper.base.BaseTopicWrapper; import org.jboss.pressgang.ccms.wrapper.collection.CollectionWrapper; import org.jboss.pressgang.ccms.zanata.ZanataDetails; import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.Entity; import org.w3c.dom.NamedNodeMap; import org.w3c.dom.Node; import org.w3c.dom.NodeList; /** * A builder to build DocBook compatible output using a Content Specification. The builder works in the following stages: * <br /> * <ol> * <li> * Populate Pass * <ul> * <li>Downloads all the topics using the REST API and creates the SpecTopic/Level database.</li> * </ul> * </li> * <li> * Topic Pass * <ul> * <li>Goes through each topic and converts the XML into a DOM element. If the XML is empty or can't be converted into a * DOM element it is replaced by a template. Then it initialises all the SpecTopics with the XML and Topic information.</li> * </ul> * </li> * <li> * Spec Topic First Pass * <ul> * <li>Goes through each SpecTopic and processes the XML to remove conditional statements. This stage also collects all of * the xml id's in the book, which are used in the next step. * </li> * </ul> * </li> * <li> * Topic First Pass * <ul> * <li>Goes through each SpecTopic and processes the XML to check that each <xref> or <link> has a valid * reference to some point in the book. * </li> * </ul> * </li> * <li> * Spec Topic Second Pass * <ul> * <li>Goes through each SpecTopic and processes the XML to resolve injections and then do a proper validation on the XML * content. It will then go through the XML and fix any possible duplicate ids (for example if the same topic is included * twice in a Content Spec). * </li> * </ul> * </li> * <li> * Link Second Pass * <ul> * <li>Goes through each SpecTopic and fixes any links where the link may no longer exist due to the linked topic being * replaced by an error template. If there is any found, than the old link is set to point to the error template. * </li> * </ul> * </li> * <li> * Build Pass * <ul> * <li>This step also builds the book structure using the content specification and goes through and converts each SpecTopics * DOM XML representation into a XML string. This step will also download any images and additional files and add them to the * output. * </li> * </ul> * </li> * </ol> */ public class DocBookBuilder implements ShutdownAbleApp { protected static final Logger log = Logger.getLogger(DocBookBuilder.class); protected static final DateFormat DATE_FORMATTER = new SimpleDateFormat("dd-MM-yyyy HH:mm:ss"); protected static final boolean INCLUDE_CHECKSUMS = false; protected static final Integer MAX_URL_LENGTH = 4000; protected static final String ENCODING = "UTF-8"; protected static final String REVISION_HISTORY_FILE_NAME = "Revision_History.xml"; protected static final String FEEDBACK_FILE_NAME = "Feedback.xml"; protected static final String AUTHOR_GROUP_FILE_NAME = "Author_Group.xml"; protected static final String PREFACE_FILE_NAME = "Preface.xml"; protected static final String LEGAL_NOTICE_FILE_NAME = "Legal_Notice.xml"; protected final AtomicBoolean isShuttingDown = new AtomicBoolean(false); protected final AtomicBoolean shutdown = new AtomicBoolean(false); protected final XMLFormatProperties xmlFormatProperties = new XMLFormatProperties(); protected final DataProviderFactory providerFactory; protected final ContentSpecProvider contentSpecProvider; protected final CSNodeProvider csNodeProvider; protected final TranslatedCSNodeProvider translatedCSNodeProvider; protected final TopicProvider topicProvider; protected final TranslatedTopicProvider translatedTopicProvider; protected final TagProvider tagProvider; protected final PropertyTagProvider propertyTagProvider; protected final StringConstantProvider stringConstantProvider; protected final BlobConstantProvider blobConstantProvider; protected final ImageProvider imageProvider; private final BlobConstantWrapper rocbookDtd; private final BlobConstantWrapper docbookRng; private final String docbook45Entities; /** * The set of Messages to use when building */ private final ResourceBundle messagesResourceBundle = ResourceBundle .getBundle("org.jboss.pressgang.ccms.contentspec.builder.Messages"); /** * The StringConstant that holds the error template for a topic with no content. */ private final StringConstantWrapper errorEmptyTopicTemplate; /** * The StringConstant that holds the error template for a topic with invalid injection references. */ private final StringConstantWrapper errorInvalidInjectionTopicTemplate; /** * The StringConstant that holds the error template for a topic that failed validation. */ private final StringConstantWrapper errorInvalidValidationTopicTemplate; /** * All data associated with the build. */ private BuildData buildData; public DocBookBuilder(final DataProviderFactory providerFactory) throws BuilderCreationException { this.providerFactory = providerFactory; contentSpecProvider = providerFactory.getProvider(ContentSpecProvider.class); csNodeProvider = providerFactory.getProvider(CSNodeProvider.class); translatedCSNodeProvider = providerFactory.getProvider(TranslatedCSNodeProvider.class); topicProvider = providerFactory.getProvider(TopicProvider.class); translatedTopicProvider = providerFactory.getProvider(TranslatedTopicProvider.class); propertyTagProvider = providerFactory.getProvider(PropertyTagProvider.class); stringConstantProvider = providerFactory.getProvider(StringConstantProvider.class); blobConstantProvider = providerFactory.getProvider(BlobConstantProvider.class); tagProvider = providerFactory.getProvider(TagProvider.class); imageProvider = providerFactory.getProvider(ImageProvider.class); final ServerEntitiesWrapper serverEntities = providerFactory.getProvider(ServerSettingsProvider.class) .getServerSettings().getEntities(); rocbookDtd = blobConstantProvider.getBlobConstant(serverEntities.getRocBook45DTDBlobConstantId()); docbookRng = blobConstantProvider.getBlobConstant(serverEntities.getDocBook50RNGBlobConstantId()); errorEmptyTopicTemplate = stringConstantProvider .getStringConstant(serverEntities.getEmptyTopicStringConstantId()); errorInvalidInjectionTopicTemplate = stringConstantProvider .getStringConstant(serverEntities.getInvalidInjectionStringConstantId()); errorInvalidValidationTopicTemplate = stringConstantProvider .getStringConstant(serverEntities.getInvalidTopicStringConstantId()); final StringConstantWrapper xmlElementsProperties = stringConstantProvider .getStringConstant(serverEntities.getXmlFormattingStringConstantId()); /* * Get the XML formatting details. These are used to pretty-print the XML when it is converted into a String. */ final Properties prop = new Properties(); try { prop.load(new StringReader(xmlElementsProperties.getValue())); } catch (IOException e) { log.error(messagesResourceBundle.getString("FAILED_READING_XML_FORMATTING")); throw new BuilderCreationException(messagesResourceBundle.getString("FAILED_READING_XML_FORMATTING")); } final String verbatimElementsString = prop.getProperty(CommonConstants.VERBATIM_XML_ELEMENTS_PROPERTY_KEY); final String inlineElementsString = prop.getProperty(CommonConstants.INLINE_XML_ELEMENTS_PROPERTY_KEY); final String contentsInlineElementsString = prop .getProperty(CommonConstants.CONTENTS_INLINE_XML_ELEMENTS_PROPERTY_KEY); xmlFormatProperties.setVerbatimElements( CollectionUtilities.toArrayList(verbatimElementsString.split("[\\s]*,[\\s]*"))); xmlFormatProperties .setInlineElements(CollectionUtilities.toArrayList(inlineElementsString.split("[\\s]*,[\\s]*"))); xmlFormatProperties.setContentsInlineElements( CollectionUtilities.toArrayList(contentsInlineElementsString.split("[\\s]*,[\\s]*"))); docbook45Entities = ResourceUtilities.resourceFileToString("/", "docbook.ent"); } protected StringConstantWrapper getErrorEmptyTopicTemplate() { return errorEmptyTopicTemplate; } protected StringConstantWrapper getErrorInvalidInjectionTopicTemplate() { return errorInvalidInjectionTopicTemplate; } protected StringConstantWrapper getErrorInvalidValidationTopicTemplate() { return errorInvalidValidationTopicTemplate; } protected ResourceBundle getMessages() { return messagesResourceBundle; } public XMLFormatProperties getXMLFormatProperties() { return xmlFormatProperties; } protected BuildData getBuildData() { return buildData; } protected void setBuildData(BuildData buildData) { this.buildData = buildData; } @Override public void shutdown() { isShuttingDown.set(true); } @Override public boolean isShutdown() { return shutdown.get(); } /** * Gets the number of warnings that occurred during the last build. * * @return The number of warnings that occurred. */ public int getNumWarnings() { int numWarnings = 0; if (getBuildData().getErrorDatabase() != null && getBuildData().getErrorDatabase().getErrors(getBuildData().getBuildLocale()) != null) { for (final TopicErrorData errorData : getBuildData().getErrorDatabase() .getErrors(getBuildData().getBuildLocale())) { numWarnings += errorData.getItemsOfType(ErrorLevel.WARNING).size(); } } return numWarnings; } /** * Gets the number of errors that occurred during the last build. * * @return The number of errors that occurred. */ public int getNumErrors() { int numErrors = 0; if (getBuildData().getErrorDatabase() != null && getBuildData().getErrorDatabase().getErrors(getBuildData().getBuildLocale()) != null) { for (final TopicErrorData errorData : getBuildData().getErrorDatabase() .getErrors(getBuildData().getBuildLocale())) { numErrors += errorData.getItemsOfType(ErrorLevel.ERROR).size(); } } return numErrors; } /** * Reset the builder so that it can build from a clean state. */ protected void resetBuilder() { setBuildData(null); } /** * Builds a DocBook Formatted Book using a Content Specification to define the structure and contents of the book. * * @param contentSpec The content specification to build from. * @param requester The user who requested the build. * @param buildingOptions The options to be used when building. * @return Returns a mapping of file names/locations to files. This HashMap can be used to build a ZIP archive. * @throws BuilderCreationException Thrown if the builder is unable to start due to incorrect passed variables. * @throws BuildProcessingException Any build issue that should not occur under normal circumstances. Ie a Template can't be * converted to a DOM Document. */ public HashMap<String, byte[]> buildBook(final ContentSpec contentSpec, final String requester, final DocBookBuildingOptions buildingOptions) throws BuilderCreationException, BuildProcessingException { return buildBook(contentSpec, requester, buildingOptions, new HashMap<String, byte[]>()); } /** * Builds a DocBook Formatted Book using a Content Specification to define the structure and contents of the book. * * @param contentSpec The content specification to build from. * @param requester The user who requested the build. * @param buildingOptions The options to be used when building. * @param zanataDetails The Zanata server details to be used when populating links * @return Returns a mapping of file names/locations to files. This HashMap can be used to build a ZIP archive. * @throws BuilderCreationException Thrown if the builder is unable to start due to incorrect passed variables. * @throws BuildProcessingException Any build issue that should not occur under normal circumstances. Ie a Template can't be * converted to a DOM Document. */ public HashMap<String, byte[]> buildTranslatedBook(final ContentSpec contentSpec, final String requester, final DocBookBuildingOptions buildingOptions, final ZanataDetails zanataDetails) throws BuilderCreationException, BuildProcessingException { return buildTranslatedBook(contentSpec, requester, buildingOptions, new HashMap<String, byte[]>(), zanataDetails); } /** * Builds a DocBook Formatted Book using a Content Specification to define the structure and contents of the book. * * @param contentSpec The content specification to build from. * @param requester The user who requested the build. * @param buildingOptions The options to be used when building. * @param overrideFiles * @return Returns a mapping of file names/locations to files. This HashMap can be used to build a ZIP archive. * @throws BuilderCreationException Thrown if the builder is unable to start due to incorrect passed variables. * @throws BuildProcessingException Any build issue that should not occur under normal circumstances. Ie a Template can't be * converted to a DOM Document. */ public HashMap<String, byte[]> buildBook(final ContentSpec contentSpec, final String requester, final DocBookBuildingOptions buildingOptions, final Map<String, byte[]> overrideFiles) throws BuilderCreationException, BuildProcessingException { return buildBook(contentSpec, requester, buildingOptions, overrideFiles, null, false); } /** * Builds a DocBook Formatted Book using a Content Specification to define the structure and contents of the book. * * @param contentSpec The content specification to build from. * @param requester The user who requested the build. * @param buildingOptions The options to be used when building. * @param overrideFiles * @param zanataDetails The Zanata server details to be used when populating links * @return Returns a mapping of file names/locations to files. This HashMap can be used to build a ZIP archive. * @throws BuilderCreationException Thrown if the builder is unable to start due to incorrect passed variables. * @throws BuildProcessingException Any build issue that should not occur under normal circumstances. Ie a Template can't be * converted to a DOM Document. */ public HashMap<String, byte[]> buildTranslatedBook(final ContentSpec contentSpec, final String requester, final DocBookBuildingOptions buildingOptions, final Map<String, byte[]> overrideFiles, final ZanataDetails zanataDetails) throws BuilderCreationException, BuildProcessingException { return buildBook(contentSpec, requester, buildingOptions, overrideFiles, zanataDetails, true); } @SuppressWarnings("unchecked") protected HashMap<String, byte[]> buildBook(final ContentSpec contentSpec, final String requester, final DocBookBuildingOptions buildingOptions, final Map<String, byte[]> overrideFiles, final ZanataDetails zanataDetails, final boolean translationBuild) throws BuilderCreationException, BuildProcessingException { if (contentSpec == null) { throw new BuilderCreationException("No content specification specified. Unable to build from nothing!"); } // Reset the builder resetBuilder(); // If there is no requester specified than set it as unknown final String fixedRequester = requester == null ? "Unknown" : requester; // Create the build data final BuildData buildData = createBuildData(fixedRequester, contentSpec, buildingOptions, zanataDetails, providerFactory, translationBuild); setBuildData(buildData); // Set the override files if any were passed if (overrideFiles != null) { buildData.getOverrideFiles().putAll(overrideFiles); } // Check if the app should be shutdown if (isShuttingDown.get()) { shutdown.set(true); return null; } // Get the entities to be used from the content spec and expand them final List<Entity> entities = ContentSpecUtilities.getContentSpecEntities(buildData.getContentSpec()); if (buildData.getBuildOptions().isResolveEntities()) { TranslationUtilities.resolveCustomContentSpecEntities(entities, buildData.getContentSpec()); } // Get the translations if (translationBuild) { pullTranslations(contentSpec, buildData.getBuildLocale()); } // Check if the app should be shutdown if (isShuttingDown.get()) { shutdown.set(true); return null; } doPopulateDatabasePass(buildData); // Check if the app should be shutdown if (isShuttingDown.get()) { shutdown.set(true); return null; } final boolean fixedUrlsSuccess = doFixedURLsPass(buildData); buildData.setUseFixedUrls(fixedUrlsSuccess); // Check if the app should be shutdown if (isShuttingDown.get()) { shutdown.set(true); return null; } final Map<SpecTopic, Set<String>> usedIdAttributes = new HashMap<SpecTopic, Set<String>>(); doSpecTopicFirstPass(buildData, usedIdAttributes, entities); // Check if the app should be shutdown if (isShuttingDown.get()) { shutdown.set(true); return null; } /* * We need to create a list of all id's in the book to check if links are valid. So generate the id attribute that are * used by topics, section and chapters. Then add any id's that were found in the topics. */ final Set<String> bookIdAttributes = buildData.getBuildDatabase().getIdAttributes(buildData); for (final Entry<SpecTopic, Set<String>> entry : usedIdAttributes.entrySet()) { bookIdAttributes.addAll(entry.getValue()); } doLinkPass(buildData, bookIdAttributes); // Check if the app should be shutdown if (isShuttingDown.get()) { shutdown.set(true); return null; } // second topic pass to set the ids and process injections doSpecTopicSecondPass(buildData, usedIdAttributes); // Check if the app should be shutdown if (isShuttingDown.get()) { shutdown.set(true); return null; } doLinkSecondPass(buildData, usedIdAttributes); // Check if the app should be shutdown if (isShuttingDown.get()) { shutdown.set(true); return null; } // Process the images in the topics processImageLocations(buildData); // Check if the app should be shutdown if (isShuttingDown.get()) { shutdown.set(true); return null; } return doBuildZipPass(buildData); } protected BuildData createBuildData(final String fixedRequester, final ContentSpec contentSpec, final DocBookBuildingOptions buildingOptions, final ZanataDetails zanataDetails, final DataProviderFactory providerFactory, final boolean translationBuild) { return new BuildData(fixedRequester, contentSpec, buildingOptions, zanataDetails, providerFactory, translationBuild); } /** * Get the translations from the REST API and replace the original strings with the values downloaded. * * @param contentSpec The Content Spec to get and replace the translations for. * @param locale The locale to pull the translations for. * @throws BuildProcessingException Thrown if an unexpected error occurs during building. */ protected void pullTranslations(final ContentSpec contentSpec, final String locale) throws BuildProcessingException { final CollectionWrapper<TranslatedContentSpecWrapper> translatedContentSpecs = providerFactory .getProvider(TranslatedContentSpecProvider.class) .getTranslatedContentSpecsWithQuery("query;" + CommonFilterConstants.ZANATA_IDS_FILTER_VAR + "=CS" + contentSpec.getId() + "-" + contentSpec.getRevision()); // Ensure that the passed content spec has a translation if (translatedContentSpecs == null || translatedContentSpecs.isEmpty()) { throw new BuildProcessingException("Unable to find any translations for Content Spec " + contentSpec.getId() + (contentSpec.getRevision() == null ? "" : (", Revision " + contentSpec.getRevision()))); } final TranslatedContentSpecWrapper translatedContentSpec = translatedContentSpecs.getItems().get(0); if (translatedContentSpec.getTranslatedNodes() != null) { final Map<String, String> translations = new HashMap<String, String>(); // Iterate over each translated node and build up the list of translated strings for the content spec. final List<TranslatedCSNodeWrapper> translatedCSNodes = translatedContentSpec.getTranslatedNodes() .getItems(); for (final TranslatedCSNodeWrapper translatedCSNode : translatedCSNodes) { // Only process nodes that have content pushed to Zanata if (!isNullOrEmpty(translatedCSNode.getOriginalString())) { if (translatedCSNode.getTranslatedStrings() != null) { final List<TranslatedCSNodeStringWrapper> translatedCSNodeStrings = translatedCSNode .getTranslatedStrings().getItems(); for (final TranslatedCSNodeStringWrapper translatedCSNodeString : translatedCSNodeStrings) { if (translatedCSNodeString.getLocale().getValue().equals(locale)) { translations.put(translatedCSNode.getOriginalString(), translatedCSNodeString.getTranslatedString()); } } } } } // Resolve any entities to make sure that the source string match final List<Entity> entities = XMLUtilities.parseEntitiesFromString(contentSpec.getEntities()); TranslationUtilities.resolveCustomContentSpecEntities(entities, translatedContentSpec.getContentSpec()); // Replace all the translated strings TranslationUtilities.replaceTranslatedStrings(translatedContentSpec.getContentSpec(), contentSpec, translations); // Set the Unique Ids so that they can be used later setTranslationUniqueIds(contentSpec, translatedContentSpec); } } /** * Sets the Translation Unique Ids on all the content spec nodes, that have a matching translated node. * * @param contentSpec The content spec that contains all the nodes to set the translation unique ids for. * @param translatedContentSpec The Translated Content Spec object, that holds details on the translated nodes. */ protected void setTranslationUniqueIds(final ContentSpec contentSpec, final TranslatedContentSpecWrapper translatedContentSpec) throws BuildProcessingException { final List<TranslatedCSNodeWrapper> translatedCSNodes = translatedContentSpec.getTranslatedNodes() .getItems(); for (final TranslatedCSNodeWrapper translatedCSNode : translatedCSNodes) { final org.jboss.pressgang.ccms.contentspec.Node node = ContentSpecUtilities .findMatchingContentSpecNode(contentSpec, translatedCSNode.getNodeId()); if (node != null) { node.setTranslationUniqueId(translatedCSNode.getId().toString()); if (node instanceof KeyValueNode && ((KeyValueNode) node).getValue() instanceof SpecTopic) { ((SpecTopic) ((KeyValueNode) node).getValue()) .setTranslationUniqueId(translatedCSNode.getId().toString()); } } else { // This shouldn't happen, but take care of it incase it does due to another bug throw new BuildProcessingException( "Unable to find a matching Content Spec Node object for Translated Node " + translatedCSNode.getId()); } } } /** * Validate all the book links in the each topic to ensure that they exist somewhere in the book. If they don't then the * topic XML is replaced with a generic error template. * * @param buildData Information and data structures for the build. * @param bookIdAttributes A set of all the id's that exist in the book. * @throws BuildProcessingException Thrown if an unexpected error occurs during building. */ @SuppressWarnings("unchecked") protected void validateTopicLinks(final BuildData buildData, final Set<String> bookIdAttributes) throws BuildProcessingException { final List<SpecTopic> topics = buildData.getBuildDatabase().getAllSpecTopics(); final Set<Integer> processedTopics = new HashSet<Integer>(); for (final SpecTopic specTopic : topics) { final BaseTopicWrapper<?> topic = specTopic.getTopic(); final Document doc = specTopic.getXMLDocument(); /* * We only to to process topics at this point and not spec topics. So check to see if the topic has all ready been * processed. */ if (!processedTopics.contains(topic.getId())) { processedTopics.add(topic.getId()); // Get the XRef links in the topic document final Set<String> linkIds = new HashSet<String>(); DocBookBuildUtilities.getTopicLinkIds(doc, linkIds); final List<String> invalidLinks = new ArrayList<String>(); for (final String linkId : linkIds) { /* * Check if the xref linkend id exists in the book. If the Tag Starts with our error syntax then we can * ignore it */ if (!bookIdAttributes.contains(linkId) && !linkId.startsWith(CommonConstants.ERROR_XREF_ID_PREFIX)) { invalidLinks.add("\"" + linkId + "\""); } } // If there were any invalid links then replace the XML with an error template and add an error message. if (!invalidLinks.isEmpty()) { final String xmlStringInCDATA = DocBookBuildUtilities.convertDocumentToCDATAFormattedString(doc, getXMLFormatProperties()); buildData.getErrorDatabase().addError(topic, ErrorType.INVALID_CONTENT, "The following link(s) " + CollectionUtilities.toSeperatedString(invalidLinks, ", ") + " don't exist. The processed XML is <programlisting>" + xmlStringInCDATA + "</programlisting>"); // Find the Topic ID final Integer topicId = topic.getTopicId(); final List<ITopicNode> buildTopics = buildData.getBuildDatabase() .getTopicNodesForTopicID(topicId); for (final ITopicNode topicNode : buildTopics) { DocBookBuildUtilities.setTopicNodeXMLForError(buildData, topicNode, getErrorInvalidValidationTopicTemplate().getValue()); } } } } } /** * Populates the SpecTopicDatabase with the SpecTopics inside the content specification. It also adds the equivalent real * topics to each SpecTopic. * * @param buildData Information and data structures for the build. * @return True if the database was populated successfully otherwise false. * @throws BuildProcessingException Thrown if an unexpected error occurs during building. */ @SuppressWarnings("unchecked") private void doPopulateDatabasePass(final BuildData buildData) throws BuildProcessingException { log.info("Doing " + buildData.getBuildLocale() + " Populate Database Pass"); final ContentSpec contentSpec = buildData.getContentSpec(); final Map<String, BaseTopicWrapper<?>> topics = new HashMap<String, BaseTopicWrapper<?>>(); if (buildData.isTranslationBuild()) { //Translations should reference an existing historical topic with the fixed urls set, so we assume this to be the case populateTranslatedTopicDatabase(buildData, topics); } else { populateDatabaseTopics(buildData, topics); } // Check if the app should be shutdown if (isShuttingDown.get()) { return; } // Add all the levels to the database DocBookBuildUtilities.addLevelsToDatabase(buildData.getBuildDatabase(), contentSpec.getBaseLevel()); // Check if the app should be shutdown if (isShuttingDown.get()) { return; } // Process the topics to make sure they are valid doTopicPass(buildData, topics); } /** * TODO * * @param buildData Information and data structures for the build. * @param topics * @return */ protected void populateDatabaseTopics(final BuildData buildData, final Map<String, BaseTopicWrapper<?>> topics) throws BuildProcessingException { final List<TopicWrapper> allTopics = new ArrayList<TopicWrapper>(); final List<TopicWrapper> latestTopics = new ArrayList<TopicWrapper>(); final List<TopicWrapper> revisionTopics = new ArrayList<TopicWrapper>(); // Calculate the ids of all the topics to get final List<ITopicNode> topicNodes = buildData.getContentSpec().getAllTopicNodes(); for (final ITopicNode topicNode : topicNodes) { // Determine which topics we need to fetch the latest topics for and which topics we need to fetch revisions for. final TopicWrapper topic; if (topicNode.getRevision() != null && !buildData.getBuildOptions().getUseLatestVersions()) { topic = topicProvider.getTopic(topicNode.getDBId(), topicNode.getRevision()); revisionTopics.add(topic); allTopics.add(topic); } else if (buildData.getBuildOptions().getMaxRevision() != null && !buildData.getBuildOptions().getUseLatestVersions()) { topic = topicProvider.getTopic(topicNode.getDBId(), buildData.getBuildOptions().getMaxRevision()); revisionTopics.add(topic); allTopics.add(topic); } else { topic = topicProvider.getTopic(topicNode.getDBId()); latestTopics.add(topic); allTopics.add(topic); } // Add the topic to the topics collection final String key = DocBookBuildUtilities.getTopicBuildKey(topic); topics.put(key, topic); buildData.getBuildDatabase().add(topicNode, key); } } /** * Gets the translated topics from the REST Interface and also creates any dummy translations for topics that have yet to be * translated. * * @param buildData Information and data structures for the build. * @param translatedTopics The translated topic collection to add translated topics to. */ private void populateTranslatedTopicDatabase(final BuildData buildData, final Map<String, BaseTopicWrapper<?>> translatedTopics) throws BuildProcessingException { final List<ITopicNode> topicNodes = buildData.getContentSpec().getAllTopicNodes(); final int showPercent = 10; final float total = topicNodes.size(); float current = 0; int lastPercent = 0; // Loop over each Topic Node in the content spec and get it's translated topic for (final ITopicNode topicNode : topicNodes) { getTranslatedTopicForTopicNode(buildData, topicNode, translatedTopics); ++current; final int percent = Math.round(current / total * 100); if (percent - lastPercent >= showPercent) { lastPercent = percent; log.info("\tPopulate " + buildData.getBuildLocale() + " Database Pass " + percent + "% Done"); } } } /** * TODO * * @param buildData Information and data structures for the build. * @param topicNode The spec topic to find the Translated Topic for. * @return */ protected void getTranslatedTopicForTopicNode(final BuildData buildData, final ITopicNode topicNode, final Map<String, BaseTopicWrapper<?>> translatedTopics) throws BuildProcessingException { final TopicWrapper topic; if (topicNode.getRevision() != null) { topic = topicProvider.getTopic(topicNode.getDBId(), topicNode.getRevision()); } else if (buildData.getBuildOptions().getMaxRevision() != null) { topic = topicProvider.getTopic(topicNode.getDBId(), buildData.getBuildOptions().getMaxRevision()); } else { topic = topicProvider.getTopic(topicNode.getDBId()); } // Check if the spec topic has a matching translated topic node, if not then create a dummy topic if (topicNode.getTranslationUniqueId() != null) { final TranslatedCSNodeWrapper translatedCSNode = translatedCSNodeProvider .getTranslatedCSNode(Integer.parseInt(topicNode.getTranslationUniqueId())); // Check if the translated node has a specific conditional translated topic, otherwise find the normal translated topic if (translatedCSNode.getTranslatedTopics() != null && !translatedCSNode.getTranslatedTopics().isEmpty()) { // Get the matching latest translated topic and pushed translated topics final Pair<TranslatedTopicWrapper, TranslatedTopicWrapper> latestTranslations = getLatestTranslations( buildData, translatedCSNode.getTranslatedTopics(), topicNode.getRevision(), topic.getLocale()); final TranslatedTopicWrapper latestTranslatedTopic = latestTranslations.getFirst(); final TranslatedTopicWrapper latestPushedTranslatedTopic = latestTranslations.getSecond(); // If the latest translation and latest pushed topic matches, then use that if not a dummy topic should be created TranslatedTopicWrapper translatedTopic = null; if (latestTranslatedTopic != null && latestPushedTranslatedTopic != null && latestPushedTranslatedTopic.getTopicRevision() .equals(latestTranslatedTopic.getTopicRevision())) { latestTranslatedTopic.setTopic(topic); translatedTopic = latestTranslatedTopic; } else if (latestPushedTranslatedTopic != null) { latestPushedTranslatedTopic.setTopic(topic); translatedTopic = createDummyTranslatedTopicFromExisting(latestPushedTranslatedTopic, buildData.getBuildLocaleWrapper()); } else { translatedTopic = createDummyTranslatedTopic(topic, buildData.getBuildLocaleWrapper()); } translatedTopic.setTranslatedCSNode(translatedCSNode); // Create the key and add the topic to the build database String key = DocBookBuildUtilities.getTranslatedTopicBuildKey(translatedTopic, translatedCSNode); translatedTopics.put(key, translatedTopic); buildData.getBuildDatabase().add(topicNode, key); } else { getLatestTranslatedTopicForTopicNode(buildData, topicNode, topic, translatedTopics); } } else { getLatestTranslatedTopicForTopicNode(buildData, topicNode, topic, translatedTopics); } } protected void getLatestTranslatedTopicForTopicNode(final BuildData buildData, final ITopicNode topicNode, final TopicWrapper topic, final Map<String, BaseTopicWrapper<?>> translatedTopics) { String key = DocBookBuildUtilities.getTopicBuildKey(topic); // If the topic has already been processed then add the spec topic and return if (translatedTopics.containsKey(key)) { buildData.getBuildDatabase().add(topicNode, key); return; } // Get the matching latest translated topic and pushed translated topics final Pair<TranslatedTopicWrapper, TranslatedTopicWrapper> latestTranslations = getLatestTranslations( buildData, topic, topicNode.getRevision()); final TranslatedTopicWrapper latestTranslatedTopic = latestTranslations.getFirst(); final TranslatedTopicWrapper latestPushedTranslatedTopic = latestTranslations.getSecond(); // If the latest translation and latest pushed topic matches, then use that if not a dummy topic should be created if (latestTranslatedTopic != null && latestPushedTranslatedTopic != null && latestPushedTranslatedTopic .getTopicRevision().equals(latestTranslatedTopic.getTopicRevision())) { final TranslatedTopicWrapper translatedTopic = translatedTopicProvider .getTranslatedTopic(latestTranslatedTopic.getId()); translatedTopics.put(key, translatedTopic); buildData.getBuildDatabase().add(topicNode, key); } else { final TranslatedTopicWrapper translatedTopic = createDummyTranslatedTopicFromTopic(topic, buildData.getBuildLocaleWrapper()); translatedTopics.put(key, translatedTopic); buildData.getBuildDatabase().add(topicNode, key); } } /** * Find the latest pushed and translated topics for a topic. We need to do this since translations are only added when some * content is added in Zanata. So if the latest translated topic doesn't match the topic revision of the latest pushed then * we will need to create a dummy topic for the latest pushed topic. * * @param buildData Information and data structures for the build. * @param topic The topic to find the latest translated topic and pushed translation. * @param rev The revision for the topic as specified in the ContentSpec. * @return A Pair whose first element is the Latest Translated Topic and second element is the Latest Pushed Translation. */ private Pair<TranslatedTopicWrapper, TranslatedTopicWrapper> getLatestTranslations(final BuildData buildData, final TopicWrapper topic, final Integer rev) { return getLatestTranslations(buildData, topic.getTranslatedTopics(), rev, topic.getLocale()); } /** * Find the latest pushed and translated topics for a topic. We need to do this since translations are only added when some * content is added in Zanata. So if the latest translated topic doesn't match the topic revision of the latest pushed then * we will need to create a dummy topic for the latest pushed topic. * * @param buildData Information and data structures for the build. * @param translatedTopics The translated topics to search find the latest translated topic and pushed translation. * @param rev The revision for the topic as specified in the ContentSpec. * @param baseLocale The original sources locale. * @return A Pair whose first element is the Latest Translated Topic and second element is the Latest Pushed Translation. */ protected Pair<TranslatedTopicWrapper, TranslatedTopicWrapper> getLatestTranslations(final BuildData buildData, final CollectionWrapper<TranslatedTopicWrapper> translatedTopics, final Integer rev, final LocaleWrapper baseLocale) { return getLatestTranslations(buildData, translatedTopics, rev, baseLocale.getValue()); } /** * Find the latest pushed and translated topics for a topic. We need to do this since translations are only added when some * content is added in Zanata. So if the latest translated topic doesn't match the topic revision of the latest pushed then * we will need to create a dummy topic for the latest pushed topic. * * @param buildData Information and data structures for the build. * @param translatedTopics The translated topics to search find the latest translated topic and pushed translation. * @param rev The revision for the topic as specified in the ContentSpec. * @param baseLocale The original sources locale. * @return A Pair whose first element is the Latest Translated Topic and second element is the Latest Pushed Translation. */ protected Pair<TranslatedTopicWrapper, TranslatedTopicWrapper> getLatestTranslations(final BuildData buildData, final CollectionWrapper<TranslatedTopicWrapper> translatedTopics, final Integer rev, final String baseLocale) { TranslatedTopicWrapper latestTranslatedTopic = null; TranslatedTopicWrapper latestPushedTranslatedTopic = null; if (translatedTopics != null && translatedTopics.getItems() != null) { final List<TranslatedTopicWrapper> topics = translatedTopics.getItems(); for (final TranslatedTopicWrapper tempTopic : topics) { // Find the Latest Translated Topic if (buildData.getBuildLocale().equals(tempTopic.getLocale().getValue()) && (latestTranslatedTopic == null || latestTranslatedTopic.getTopicRevision() < tempTopic.getTopicRevision()) && (rev == null || tempTopic.getTopicRevision() <= rev)) { latestTranslatedTopic = tempTopic; } // Find the Latest Pushed Topic if (baseLocale.equals(tempTopic.getLocale().getValue()) && (latestPushedTranslatedTopic == null || latestPushedTranslatedTopic.getTopicRevision() < tempTopic.getTopicRevision()) && (rev == null || tempTopic.getTopicRevision() <= rev)) { latestPushedTranslatedTopic = tempTopic; } } } return new Pair<TranslatedTopicWrapper, TranslatedTopicWrapper>(latestTranslatedTopic, latestPushedTranslatedTopic); } /** * Creates a dummy translated topic so that a book can be built using the same relationships as a normal build. * * @param topic The topic to create the dummy topic from. * @param locale The locale to build the dummy translations for. * @return The dummy translated topic. */ private TranslatedTopicWrapper createDummyTranslatedTopicFromTopic(final TopicWrapper topic, final LocaleWrapper locale) { final TranslatedTopicWrapper pushedTranslatedTopic = EntityUtilities.returnPushedTranslatedTopic(topic); /* * Try and use the untranslated default locale translated topic as the base for the dummy topic. If that fails then * create a dummy topic from the passed RESTTopicV1. */ if (pushedTranslatedTopic != null) { pushedTranslatedTopic.setTopic(topic); pushedTranslatedTopic.setTags(topic.getTags()); pushedTranslatedTopic.setProperties(topic.getProperties()); return createDummyTranslatedTopicFromExisting(pushedTranslatedTopic, locale); } else { return createDummyTranslatedTopic(topic, locale); } } /** * Creates a dummy translated topic so that a book can be built using the same relationships as a normal build. * * @param topic The topic to create the dummy topic from. * @param locale The locale to build the dummy translations for. * @return The dummy translated topic. */ private TranslatedTopicWrapper createDummyTranslatedTopic(final TopicWrapper topic, final LocaleWrapper locale) { final TranslatedTopicWrapper translatedTopic = translatedTopicProvider.newTranslatedTopic(); translatedTopic.setTopic(topic); translatedTopic.setId(topic.getId() * -1); // If we get to this point then no translation exists or the default locale translation failed to be downloaded. translatedTopic.setTopicId(topic.getId()); translatedTopic.setTopicRevision(topic.getRevision()); translatedTopic.setTranslationPercentage(100); translatedTopic.setXml(topic.getXml()); translatedTopic.setTags(topic.getTags()); translatedTopic.setSourceURLs(topic.getSourceURLs()); translatedTopic.setProperties(topic.getProperties()); translatedTopic.setLocale(locale); translatedTopic.setTitle(topic.getTitle()); return translatedTopic; } /** * Creates a dummy translated topic from an existing translated topic so that a book can be built using the same relationships as a * normal build. * * @param translatedTopic The translated topic to create the dummy translated topic from. * @param locale The locale to build the dummy translations for. * @return The dummy translated topic. */ private TranslatedTopicWrapper createDummyTranslatedTopicFromExisting( final TranslatedTopicWrapper translatedTopic, final LocaleWrapper locale) { // Make sure some collections are loaded, so the clone works properly translatedTopic.getTags(); translatedTopic.getProperties(); // Clone the existing version final TranslatedTopicWrapper defaultLocaleTranslatedTopic = translatedTopic.clone(false); // Negate the ID to show it isn't a proper translated topic defaultLocaleTranslatedTopic.setId(translatedTopic.getTopicId() * -1); // Change the locale since the default locale translation is being transformed into a dummy translation defaultLocaleTranslatedTopic.setLocale(locale); return defaultLocaleTranslatedTopic; } /** * Do the first topic pass on the database and check if the base XML is valid and set the Document Object's for each spec * topic. Also collect the ID Attributes that are used within the topics. * * @param buildData Information and data structures for the build. * @param topics The list of topics to be checked and added to the database. * @throws BuildProcessingException Thrown if an unexpected error occurs during building. */ private void doTopicPass(final BuildData buildData, final Map<String, BaseTopicWrapper<?>> topics) throws BuildProcessingException { log.info("Doing " + buildData.getBuildLocale() + " First topic pass"); // Check that we have some topics to process if (topics != null) { log.info("\tProcessing " + topics.size() + " Topics"); final int showPercent = 10; final float total = topics.size(); float current = 0; int lastPercent = 0; // Process each topic for (final Entry<String, BaseTopicWrapper<?>> topicEntry : topics.entrySet()) { final BaseTopicWrapper<?> topic = topicEntry.getValue(); final String key = topicEntry.getKey(); ++current; final int percent = Math.round(current / total * 100); if (percent - lastPercent >= showPercent) { lastPercent = percent; log.info("\tFirst topic Pass " + percent + "% Done"); } // Get the Topic ID final Integer topicRevision = topic.getTopicRevision(); boolean revHistoryTopic = topic.hasTag(buildData.getServerEntities().getRevisionHistoryTagId()); boolean legalNoticeTopic = topic.hasTag(buildData.getServerEntities().getLegalNoticeTagId()); boolean authorGroupTopic = topic.hasTag(buildData.getServerEntities().getAuthorGroupTagId()); boolean abstractTopic = topic.hasTag(buildData.getServerEntities().getAbstractTagId()); boolean infoTopic = topic.hasTag(buildData.getServerEntities().getInfoTagId()); Document topicDoc = null; final String topicXML = topic.getXml(); // Check if the app should be shutdown if (isShuttingDown.get()) { return; } boolean xmlValid = true; // Check that the Topic XML exists and isn't empty if (topicXML == null || topicXML.trim().isEmpty()) { buildData.getErrorDatabase().addWarning(topic, ErrorType.NO_CONTENT, BuilderConstants.WARNING_EMPTY_TOPIC_XML); topicDoc = DocBookBuildUtilities.setTopicXMLForError(buildData, topic, getErrorEmptyTopicTemplate().getValue()); xmlValid = false; } // Check if the app should be shutdown if (isShuttingDown.get()) { return; } // Make sure we have valid XML if (xmlValid) { try { final String fixedTopicXML; if (buildData.getDocBookVersion() == DocBookVersion.DOCBOOK_50) { fixedTopicXML = DocBookUtilities.addDocBook50Namespace(topicXML); } else { fixedTopicXML = topicXML; } topicDoc = XMLUtilities.convertStringToDocument(fixedTopicXML); if (topicDoc == null) { final String xmlStringInCDATA = XMLUtilities.wrapStringInCDATA(topic.getXml()); buildData.getErrorDatabase().addError(topic, ErrorType.INVALID_CONTENT, BuilderConstants.ERROR_INVALID_XML_CONTENT + " The processed XML is <programlisting>" + xmlStringInCDATA + "</programlisting>"); topicDoc = DocBookBuildUtilities.setTopicXMLForError(buildData, topic, getErrorInvalidValidationTopicTemplate().getValue()); } } catch (Exception ex) { final String xmlStringInCDATA = XMLUtilities.wrapStringInCDATA(topic.getXml()); buildData.getErrorDatabase().addError(topic, ErrorType.INVALID_CONTENT, BuilderConstants.ERROR_BAD_XML_STRUCTURE + " " + StringUtilities.escapeForXML(ex.getMessage()) + " The processed XML is <programlisting>" + xmlStringInCDATA + "</programlisting>"); topicDoc = DocBookBuildUtilities.setTopicXMLForError(buildData, topic, getErrorInvalidValidationTopicTemplate().getValue()); } } // Make sure the topic has the correct root element and other items if (revHistoryTopic) { // If it is a translated build then check if we have anything more to merge together if (buildData.isTranslationBuild()) { topicDoc = mergeAdditionalTranslatedXML(buildData, topicDoc, (TranslatedTopicWrapper) topic, TopicType.REVISION_HISTORY); } DocBookUtilities.wrapDocumentInAppendix(topicDoc); } else if (authorGroupTopic) { // If it is a translated build then check if we have anything more to merge together if (buildData.isTranslationBuild()) { topicDoc = mergeAdditionalTranslatedXML(buildData, topicDoc, (TranslatedTopicWrapper) topic, TopicType.AUTHOR_GROUP); } DocBookUtilities.wrapDocumentInAuthorGroup(topicDoc); } else if (legalNoticeTopic) { DocBookUtilities.wrapDocumentInLegalNotice(topicDoc); } else if (abstractTopic) { DocBookUtilities.wrapDocument(topicDoc, "abstract"); } else if (infoTopic) { if (buildData.getDocBookVersion() == DocBookVersion.DOCBOOK_50) { DocBookUtilities.wrapDocument(topicDoc, "info"); } else { DocBookUtilities.wrapDocument(topicDoc, "sectioninfo"); } } else { // Ensure the topic is wrapped in a section and the title matches the topic DocBookUtilities.wrapDocumentInSection(topicDoc); DocBookUtilities.setSectionTitle(buildData.getDocBookVersion(), topic.getTitle(), topicDoc); processTopicSectionInfo(buildData, topic, topicDoc); } // Add the document & topic to the database spec topics final List<ITopicNode> specTopics = buildData.getBuildDatabase().getTopicNodesForKey(key); for (final ITopicNode specTopic : specTopics) { // Check if the app should be shutdown if (isShuttingDown.get()) { return; } if (buildData.getBuildOptions().getUseLatestVersions()) { specTopic.setTopic(topic.clone(false)); specTopic.setXMLDocument((Document) topicDoc.cloneNode(true)); } else { /* * Only set the topic for the spec topic if it matches the spec topic revision. If the Spec Topic * Revision is null then we need to ensure that we get the latest version of the topic that was * downloaded. */ if ((specTopic.getRevision() == null && (specTopic.getTopic() == null || specTopic.getTopic().getRevision() >= topicRevision)) || (specTopic.getRevision() != null && specTopic.getRevision() >= topicRevision)) { specTopic.setTopic(topic.clone(false)); specTopic.setXMLDocument((Document) topicDoc.cloneNode(true)); } } } } } else { log.info("\tProcessing 0 Topics"); } } /** * Merges the Additional Translated XML of a Translated Topic into the original Topic XML content. * * @param buildData * @param topicDoc The transformed original XML content. * @param translatedTopic The Translated Topic that is being merged. * @param topicType The type of topic being merged. * @return The merged DOM document. * @throws BuildProcessingException */ private Document mergeAdditionalTranslatedXML(BuildData buildData, final Document topicDoc, final TranslatedTopicWrapper translatedTopic, final TopicType topicType) throws BuildProcessingException { Document retValue = topicDoc; if (!isNullOrEmpty(translatedTopic.getTranslatedAdditionalXML())) { Document additionalXMLDoc = null; try { additionalXMLDoc = XMLUtilities .convertStringToDocument(translatedTopic.getTranslatedAdditionalXML()); } catch (Exception ex) { buildData.getErrorDatabase().addError(translatedTopic, ErrorType.INVALID_CONTENT, BuilderConstants.ERROR_INVALID_TOPIC_XML + " " + StringUtilities.escapeForXML(ex.getMessage())); retValue = DocBookBuildUtilities.setTopicXMLForError(buildData, translatedTopic, getErrorInvalidValidationTopicTemplate().getValue()); } if (additionalXMLDoc != null) { // Merge the two together try { if (TopicType.AUTHOR_GROUP.equals(topicType)) { DocBookBuildUtilities.mergeAuthorGroups(topicDoc, additionalXMLDoc); } else if (TopicType.REVISION_HISTORY.equals(topicType)) { DocBookBuildUtilities.mergeRevisionHistories(topicDoc, additionalXMLDoc); } } catch (BuildProcessingException ex) { final String xmlStringInCDATA = XMLUtilities .wrapStringInCDATA(translatedTopic.getTranslatedAdditionalXML()); buildData.getErrorDatabase().addError(translatedTopic, ErrorType.INVALID_CONTENT, BuilderConstants.ERROR_BAD_XML_STRUCTURE + " " + StringUtilities.escapeForXML(ex.getMessage()) + " The processed XML is <programlisting>" + xmlStringInCDATA + "</programlisting>"); retValue = DocBookBuildUtilities.setTopicXMLForError(buildData, translatedTopic, getErrorInvalidValidationTopicTemplate().getValue()); } } else { final String xmlStringInCDATA = XMLUtilities .wrapStringInCDATA(translatedTopic.getTranslatedAdditionalXML()); buildData.getErrorDatabase().addError(translatedTopic, ErrorType.INVALID_CONTENT, BuilderConstants.ERROR_INVALID_XML_CONTENT + " The processed XML is <programlisting>" + xmlStringInCDATA + "</programlisting>"); retValue = DocBookBuildUtilities.setTopicXMLForError(buildData, translatedTopic, getErrorInvalidValidationTopicTemplate().getValue()); } } return retValue; } /** * Loops through each of the spec topics in the database and sets the injections and unique ids for each id attribute in the * Topics XML. * * @param buildData Information and data structures for the build. * @param usedIdAttributes The set of ids that have been used in the set of topics in the content spec. * @throws BuildProcessingException Thrown if an unexpected error occurs during building. */ @SuppressWarnings("unchecked") private <T extends BaseTopicWrapper<T>> void doSpecTopicFirstPass(final BuildData buildData, final Map<SpecTopic, Set<String>> usedIdAttributes, final List<Entity> entities) throws BuildProcessingException { final List<SpecTopic> specTopics = buildData.getBuildDatabase().getAllSpecTopics(); for (final SpecTopic specTopic : specTopics) { // Check if the app should be shutdown if (isShuttingDown.get()) { return; } if (log.isDebugEnabled()) log.debug("\tProcessing SpecTopic " + specTopic.getId() + (specTopic.getRevision() != null ? (", " + "Revision " + specTopic.getRevision()) : "")); final BaseTopicWrapper<?> topic = specTopic.getTopic(); final Document doc = specTopic.getXMLDocument(); assert doc != null; assert topic != null; if (doc != null) { // Set the root element ID DocBookBuildUtilities.processTopicID(buildData, specTopic, doc); // Resolve the entities if required if (buildData.getBuildOptions().isResolveEntities()) { try { TranslationUtilities.resolveCustomTopicEntities(entities, doc); } catch (Exception e) { // Do Nothing } } // Process the conditional statements processConditions(buildData, specTopic, doc); /* * If the topic is a translated topic then check to see if the translated topic hasn't been pushed for * translation, is untranslated, has incomplete translations or contains fuzzy text. */ if (topic instanceof TranslatedTopicWrapper) { // Check the topic itself isn't a dummy topic if (EntityUtilities.isDummyTopic(topic) && EntityUtilities.hasBeenPushedForTranslation((TranslatedTopicWrapper) topic)) { buildData.getErrorDatabase().addWarning(topic, ErrorType.UNTRANSLATED, BuilderConstants.WARNING_UNTRANSLATED_TOPIC); } else if (EntityUtilities.isDummyTopic(topic)) { buildData.getErrorDatabase().addWarning(topic, ErrorType.NOT_PUSHED_FOR_TRANSLATION, BuilderConstants.WARNING_NONPUSHED_TOPIC); } else { // Check if the topic's content isn't fully translated if (((TranslatedTopicWrapper) topic).getTranslationPercentage() < 100) { buildData.getErrorDatabase().addWarning(topic, ErrorType.INCOMPLETE_TRANSLATION, BuilderConstants.WARNING_INCOMPLETE_TRANSLATION); } if (((TranslatedTopicWrapper) topic).getContainsFuzzyTranslations()) { buildData.getErrorDatabase().addWarning(topic, ErrorType.FUZZY_TRANSLATION, BuilderConstants.WARNING_FUZZY_TRANSLATION); } } } // Check if the app should be shutdown if (isShuttingDown.get()) { return; } // Check to see if the translated topic revision is an older topic than the topic revision specified in the map if (topic instanceof TranslatedTopicWrapper) { final TranslatedTopicWrapper pushedTranslatedTopic = EntityUtilities .returnPushedTranslatedTopic((TranslatedTopicWrapper) topic); if (pushedTranslatedTopic != null && specTopic.getRevision() != null && !pushedTranslatedTopic.getTopicRevision().equals(specTopic.getRevision())) { if (EntityUtilities.isDummyTopic(topic)) { buildData.getErrorDatabase().addWarning(topic, ErrorType.OLD_UNTRANSLATED, BuilderConstants.WARNING_OLD_UNTRANSLATED_TOPIC); } else { buildData.getErrorDatabase().addWarning(topic, ErrorType.OLD_TRANSLATION, BuilderConstants.WARNING_OLD_TRANSLATED_TOPIC); } } } /* * Extract the id attributes used in this topic. We'll use this data in the second pass to make sure that * individual topics don't repeat id attributes. */ DocBookBuildUtilities.collectIdAttributes(buildData.getDocBookVersion(), specTopic, doc, usedIdAttributes); } } } /** * Checks if the conditional pass should be performed. * * @param buildData Information and data structures for the build. * @param specTopic The spec topic the conditions should be processed for, * @param doc The DOM Document to process the conditions against. */ protected void processConditions(final BuildData buildData, final SpecTopic specTopic, final Document doc) { final String condition = specTopic.getConditionStatement(true); DocBookUtilities.processConditions(condition, doc, BuilderConstants.DEFAULT_CONDITION); } /** * Go through each topic and ensure that the links are valid, and then sets the duplicate ids for the SpecTopics/Levels * * @param buildData * @param bookIdAttributes The set of ids that have been used in the set of topics in the content spec. * @throws BuildProcessingException */ protected void doLinkPass(final BuildData buildData, final Set<String> bookIdAttributes) throws BuildProcessingException { log.info("Doing " + buildData.getBuildLocale() + " Topic Link Pass"); validateTopicLinks(buildData, bookIdAttributes); // Apply the duplicate ids for the spec topics buildData.getBuildDatabase().setDatabaseDuplicateIds(); } /** * Loops through each of the spec topics in the database and sets the injections and unique ids for each id attribute in the * Topics XML. * * @param buildData Information and data structures for the build. * @param usedIdAttributes The set of ids that have been used in the set of topics in the content spec. * @throws BuildProcessingException Thrown if an unexpected error occurs during building. */ @SuppressWarnings("unchecked") private <T extends BaseTopicWrapper<T>> void doSpecTopicSecondPass(final BuildData buildData, final Map<SpecTopic, Set<String>> usedIdAttributes) throws BuildProcessingException { log.info("Doing " + buildData.getBuildLocale() + " Spec Topic Pass"); final List<ITopicNode> topicNodes = buildData.getBuildDatabase().getAllTopicNodes(); log.info("\tProcessing " + topicNodes.size() + " Spec Topics"); final int showPercent = 10; final float total = topicNodes.size(); float current = 0; int lastPercent = 0; final DocBookXMLPreProcessor xmlPreProcessor = buildData.getXMLPreProcessor(); for (final ITopicNode topicNode : topicNodes) { // Check if the app should be shutdown if (isShuttingDown.get()) { return; } if (log.isDebugEnabled()) log.debug("\tProcessing SpecTopic " + topicNode.getId() + (topicNode.getRevision() != null ? (", " + "Revision " + topicNode.getRevision()) : "")); ++current; final int percent = Math.round(current / total * 100); if (percent - lastPercent >= showPercent) { lastPercent = percent; log.info("\tProcessing Pass " + percent + "% Done"); } final BaseTopicWrapper<?> topic = topicNode.getTopic(); final Document doc = topicNode.getXMLDocument(); assert doc != null; assert topic != null; if (doc != null) { final boolean valid = processSpecTopicInjections(buildData, topicNode, xmlPreProcessor); // Check if the app should be shutdown if (isShuttingDown.get()) { return; } if (!valid) { final String xmlStringInCDATA = DocBookBuildUtilities.convertDocumentToCDATAFormattedString(doc, getXMLFormatProperties()); buildData.getErrorDatabase().addError(topic, BuilderConstants.ERROR_INVALID_INJECTIONS + " The processed XML is <programlisting>" + xmlStringInCDATA + "</programlisting>"); DocBookBuildUtilities.setTopicNodeXMLForError(buildData, topicNode, getErrorInvalidInjectionTopicTemplate().getValue()); } else { // Check for any possible invalid injection references final List<InjectionError> injectionErrors = XMLUtilities.checkForInvalidInjections(doc); if (!injectionErrors.isEmpty()) { for (final InjectionError injectionError : injectionErrors) { final List<String> injectionErrorMsgs = new ArrayList<String>(); for (final String msg : injectionError.getMessages()) { injectionErrorMsgs.add(DocBookUtilities.buildListItem(msg)); } final String errorMsg = "\"" + injectionError.getInjection().trim() + "\" " + BuilderConstants.WARNING_POSSIBLE_INVALID_INJECTIONS + DocBookUtilities.wrapListItems(injectionErrorMsgs); buildData.getErrorDatabase().addWarning(topic, ErrorType.POSSIBLE_INVALID_INJECTION, errorMsg); } } } // Ensure that all of the id attributes are valid by setting any duplicates with a post fixed number. DocBookBuildUtilities.setUniqueIds(buildData, topicNode, topicNode.getXMLDocument().getDocumentElement(), topicNode.getXMLDocument(), usedIdAttributes); // Make sure the XML is valid docbook after the standard processing has been done if (validateTopicXML(buildData, topicNode, doc) && topicNode instanceof SpecTopic) { // Add the editor/report a bug links (these should always be valid) xmlPreProcessor.processTopicAdditionalInfo(buildData, (SpecTopic) topicNode, doc); } else { // Re-run the unique id pass, as the topic would have been replaced by an error template DocBookBuildUtilities.setUniqueIds(buildData, topicNode, topicNode.getXMLDocument().getDocumentElement(), topicNode.getXMLDocument(), usedIdAttributes); } } } } /** * Fixes any topics links that have been broken due to the linked topics XML being invalid. * * @param buildData Information and data structures for the build. * @param usedIdAttributes The set of ids that have been used in the set of topics in the content spec. * @throws BuildProcessingException */ protected void doLinkSecondPass(final BuildData buildData, final Map<SpecTopic, Set<String>> usedIdAttributes) throws BuildProcessingException { final List<SpecTopic> topics = buildData.getBuildDatabase().getAllSpecTopics(); for (final SpecTopic specTopic : topics) { final Document doc = specTopic.getXMLDocument(); // Get the XRef links in the topic document final Set<String> linkIds = new HashSet<String>(); DocBookBuildUtilities.getTopicLinkIds(doc, linkIds); final Map<String, SpecTopic> invalidLinks = new HashMap<String, SpecTopic>(); for (final String linkId : linkIds) { // Ignore error links if (linkId.startsWith(CommonConstants.ERROR_XREF_ID_PREFIX)) continue; // Find the linked topic SpecTopic linkedTopic = null; for (final Map.Entry<SpecTopic, Set<String>> usedIdEntry : usedIdAttributes.entrySet()) { if (usedIdEntry.getValue().contains(linkId)) { linkedTopic = usedIdEntry.getKey(); break; } } // If the linked topic has been set as an error, than update the links to point to the topic id if (linkedTopic != null && buildData.getErrorDatabase().hasErrorData(linkedTopic.getTopic())) { final TopicErrorData errorData = buildData.getErrorDatabase() .getErrorData(linkedTopic.getTopic()); if (errorData.hasFatalErrors()) { invalidLinks.put(linkId, linkedTopic); } } } // Go through and fix any invalid links if (!invalidLinks.isEmpty()) { final List<Node> linkNodes = XMLUtilities.getChildNodes(doc, "xref", "link"); for (final Node linkNode : linkNodes) { final String linkId = ((Element) linkNode).getAttribute("linkend"); if (invalidLinks.containsKey(linkId)) { final SpecTopic linkedTopic = invalidLinks.get(linkId); ((Element) linkNode).setAttribute("linkend", linkedTopic.getUniqueLinkId(buildData.isUseFixedUrls())); } } } } } /** * Process the Injections for a SpecTopic and add any errors to the error database. * * @param buildData Information and data structures for the build. * @param topicNode The Build Topic to do injection processing on. * @param xmlPreProcessor The XML Processor to use for Injections. * @return True if no errors occurred or if the build is set to ignore missing injections, otherwise false. */ @SuppressWarnings("unchecked") protected boolean processSpecTopicInjections(final BuildData buildData, final ITopicNode topicNode, final DocBookXMLPreProcessor xmlPreProcessor) { final BaseTopicWrapper<?> topic = topicNode.getTopic(); final Document doc = topicNode.getXMLDocument(); final boolean useFixedUrls = buildData.isUseFixedUrls(); boolean valid = true; // Process the injection points if (buildData.getInjectionOptions().isInjectionAllowed()) { final ArrayList<String> customInjectionIds = new ArrayList<String>(); if (topicNode instanceof SpecTopic) { final SpecTopic specTopic = (SpecTopic) topicNode; xmlPreProcessor.processPrevRelationshipInjections(specTopic, doc, useFixedUrls); xmlPreProcessor.processNextRelationshipInjections(specTopic, doc, useFixedUrls); // Front Matter topics are injected later as they need to be grouped if (topicNode.getTopicType() != TopicType.INITIAL_CONTENT) { xmlPreProcessor.processPrerequisiteInjections(specTopic, doc, useFixedUrls); xmlPreProcessor.processLinkListRelationshipInjections(specTopic, doc, useFixedUrls); xmlPreProcessor.processSeeAlsoInjections(specTopic, doc, useFixedUrls); } } // Process the topics XML and insert the injection links final List<String> customInjectionErrors = xmlPreProcessor.processInjections(buildData.getContentSpec(), topicNode, customInjectionIds, doc, buildData.getBuildOptions(), buildData.getBuildDatabase(), useFixedUrls); // Check if the app should be shutdown if (isShuttingDown.get()) { return false; } // Handle any errors that occurred while processing the injections valid = processSpecTopicInjectionErrors(buildData, topic, customInjectionErrors); } return valid; } /** * Process the Injections for a SpecTopic and add any errors to the error database. * * @param buildData Information and data structures for the build. * @param level * @param doc * @param xmlPreProcessor The XML Processor to use for Injections. */ protected void processLevelInjections(final BuildData buildData, final Level level, final Document doc, final Element node, final DocBookXMLPreProcessor xmlPreProcessor) { final boolean useFixedUrls = buildData.isUseFixedUrls(); // Process the injection points if (buildData.getInjectionOptions().isInjectionAllowed()) { xmlPreProcessor.processPrerequisiteInjections(level, doc, node, useFixedUrls); xmlPreProcessor.processLinkListRelationshipInjections(level, doc, node, useFixedUrls); xmlPreProcessor.processSeeAlsoInjections(level, doc, node, useFixedUrls); } } /** * Process the Injection Errors and add them to the Error Database. * * @param buildData Information and data structures for the build. * @param topic The topic that the errors occurred for. * @param customInjectionErrors The List of Custom Injection Errors. * @return True if no errors were processed or if the build is set to ignore missing injections, otherwise false. */ protected boolean processSpecTopicInjectionErrors(final BuildData buildData, final BaseTopicWrapper<?> topic, final List<String> customInjectionErrors) { boolean valid = true; if (!customInjectionErrors.isEmpty()) { final String message = "Topic has referenced Topic/Level(s) " + CollectionUtilities.toSeperatedString(customInjectionErrors) + " in a custom injection point that was not included this book."; if (buildData.getBuildOptions().getIgnoreMissingCustomInjections()) { buildData.getErrorDatabase().addWarning(topic, ErrorType.INVALID_INJECTION, message); } else { buildData.getErrorDatabase().addError(topic, ErrorType.INVALID_INJECTION, message); valid = false; } } return valid; } /** * Wrap all of the topics, images, common content, etc... files into a ZIP Archive. * * @param buildData Information and data structures for the build. * @return A ZIP Archive containing all the information to build the book. * @throws BuildProcessingException Any build issue that should not occur under normal circumstances. Ie a Template can't be * converted to a DOM Document. */ protected HashMap<String, byte[]> doBuildZipPass(final BuildData buildData) throws BuildProcessingException { log.info("Building the ZIP file"); // Add the base book information final HashMap<String, byte[]> files = buildData.getOutputFiles(); // Add the additional files buildBookAdditions(buildData); // add the images to the book addImagesToBook(buildData); // add any additional files to the book addAdditionalFilesToBook(buildData); // Build the book base buildBookBase(buildData); return files; } /** * Builds the Book.xml file of a Docbook from the resource files for a specific content specification. * * @param buildData * @throws BuildProcessingException */ protected void buildBookBase(final BuildData buildData) throws BuildProcessingException { final ContentSpec contentSpec = buildData.getContentSpec(); // Get the template from the server final String bookXmlTemplate; if (contentSpec.getBookType() == BookType.ARTICLE || contentSpec.getBookType() == BookType.ARTICLE_DRAFT) { bookXmlTemplate = stringConstantProvider .getStringConstant(buildData.getServerEntities().getArticleStringConstantId()).getValue(); } else { bookXmlTemplate = stringConstantProvider .getStringConstant(buildData.getServerEntities().getBookStringConstantId()).getValue(); } // Setup the basic book.xml String basicBook = bookXmlTemplate.replaceAll(BuilderConstants.ESCAPED_TITLE_REGEX, buildData.getEscapedBookTitle()); basicBook = basicBook.replaceAll(BuilderConstants.PRODUCT_REGEX, DocBookBuildUtilities.getKeyValueNodeText(buildData, contentSpec.getProductNode())); basicBook = basicBook.replaceAll(BuilderConstants.VERSION_REGEX, DocBookBuildUtilities.getKeyValueNodeText(buildData, contentSpec.getVersionNode())); basicBook = basicBook.replaceAll(BuilderConstants.DRAFT_REGEX, buildData.getBuildOptions().getDraft() ? "status=\"draft\"" : ""); if (contentSpec.getUseDefaultPreface()) { // Add the preface to the book.xml basicBook = basicBook.replaceAll(BuilderConstants.PREFACE_REGEX, "<xi:include href=\"Preface.xml\" xmlns:xi=\"http://www.w3.org/2001/XInclude\" />"); } // Remove the Injection sequence as we'll add the revision history and xiinclude element later basicBook = basicBook.replaceAll(BuilderConstants.XIINCLUDES_INJECTION_STRING, ""); basicBook = basicBook.replaceAll(BuilderConstants.REV_HISTORY_REGEX, ""); // Create the Book.xml DOM Document Document bookBase = null; try { // Find and Remove the Doctype first final String doctype = XMLUtilities.findDocumentType(basicBook); bookBase = XMLUtilities .convertStringToDocument(doctype == null ? basicBook : basicBook.replace(doctype, "")); } catch (Exception e) { throw new BuildProcessingException(e); } boolean flattenStructure = buildData.getBuildOptions().isServerBuild() || buildData.getBuildOptions().getFlatten(); final List<org.jboss.pressgang.ccms.contentspec.Node> levelData = contentSpec.getBaseLevel() .getChildNodes(); // Loop through and create each chapter and the topics inside those chapters log.info("\tBuilding Level and Topic XML Files"); for (final org.jboss.pressgang.ccms.contentspec.Node node : levelData) { // Check if the app should be shutdown if (isShuttingDown.get()) { return; } if (node instanceof Level) { final Level level = (Level) node; if (level instanceof InitialContent && (level.hasSpecTopics() || level.hasCommonContents())) { addLevelsInitialContent(buildData, (InitialContent) level, bookBase, bookBase.getDocumentElement(), false); } else if (level.hasSpecTopics() || level.hasCommonContents()) { // If the book is an article than just include it directly and don't create a new file if (contentSpec.getBookType() == BookType.ARTICLE || contentSpec.getBookType() == BookType.ARTICLE_DRAFT) { // Create the section and its title final Element sectionNode = bookBase.createElement("section"); setUpRootElement(buildData, level, bookBase, sectionNode); createContainerXML(buildData, level, bookBase, sectionNode, buildData.getBookTopicsFolder(), flattenStructure); bookBase.getDocumentElement().appendChild(sectionNode); } else { final Element xiInclude = createRootContainerXML(buildData, bookBase, level, flattenStructure); if (xiInclude != null) { bookBase.getDocumentElement().appendChild(xiInclude); } } } else if (buildData.getBuildOptions().isAllowEmptySections()) { final Element para = bookBase.createElement("para"); para.setTextContent("No Content"); bookBase.getDocumentElement().appendChild(para); } } else if (node instanceof SpecTopic) { final SpecTopic specTopic = (SpecTopic) node; final Node topicNode = createTopicDOMNode(specTopic, bookBase, flattenStructure, buildData.getBookTopicsFolder()); // Add the node to the Book if (topicNode != null) { bookBase.getDocumentElement().appendChild(topicNode); } } else if (node instanceof CommonContent) { final Node xiInclude = XMLUtilities.createXIInclude(bookBase, "Common_Content/" + ((CommonContent) node).getFixedTitle()); bookBase.getDocumentElement().appendChild(xiInclude); } } // Insert the editor link for the content spec if it's a translation if (buildData.getBuildOptions().getInsertEditorLinks() && buildData.isTranslationBuild()) { final String translateLinkChapter = buildTranslateCSChapter(buildData); buildData.getOutputFiles().put(buildData.getBookLocaleFolder() + "Translate.xml", StringUtilities.getStringBytes(StringUtilities .cleanTextForXML(translateLinkChapter == null ? "" : translateLinkChapter))); // Create and append the XI Include element final Element translateXMLNode = XMLUtilities.createXIInclude(bookBase, "Translate.xml"); bookBase.getDocumentElement().appendChild(translateXMLNode); } // Add any compiler errors if (!buildData.getBuildOptions().getSuppressErrorsPage() && buildData.getErrorDatabase().hasItems(buildData.getBuildLocale())) { final String compilerOutput = buildErrorChapter(buildData); buildData.getOutputFiles().put(buildData.getBookLocaleFolder() + "Errors.xml", StringUtilities .getStringBytes(StringUtilities.cleanTextForXML(compilerOutput == null ? "" : compilerOutput))); // Create and append the XI Include element final Element translateXMLNode = XMLUtilities.createXIInclude(bookBase, "Errors.xml"); bookBase.getDocumentElement().appendChild(translateXMLNode); } // Add the report chapter if (buildData.getBuildOptions().getShowReportPage()) { final String compilerOutput = buildReportChapter(buildData); buildData.getOutputFiles().put(buildData.getBookLocaleFolder() + "Report.xml", StringUtilities .getStringBytes(StringUtilities.cleanTextForXML(compilerOutput == null ? "" : compilerOutput))); // Create and append the XI Include element final Element translateXMLNode = XMLUtilities.createXIInclude(bookBase, "Report.xml"); bookBase.getDocumentElement().appendChild(translateXMLNode); } // Build the content specification page if (!buildData.getBuildOptions().getSuppressContentSpecPage()) { final String contentSpecPage = DocBookUtilities.buildAppendix(DocBookUtilities.wrapInPara( "<programlisting>" + XMLUtilities.wrapStringInCDATA(contentSpec.toString(INCLUDE_CHECKSUMS)) + "</programlisting>"), "Build Content Specification"); addToZip(buildData.getBookLocaleFolder() + "Build_Content_Specification.xml", DocBookBuildUtilities.addDocBookPreamble(buildData.getDocBookVersion(), contentSpecPage, "appendix", buildData.getEntityFileName()), buildData); // Create and append the XI Include element final Element translateXMLNode = XMLUtilities.createXIInclude(bookBase, "Build_Content_Specification.xml"); bookBase.getDocumentElement().appendChild(translateXMLNode); } // Add the revision history to the book.xml final Element revisionHistoryXMLNode = XMLUtilities.createXIInclude(bookBase, "Revision_History.xml"); bookBase.getDocumentElement().appendChild(revisionHistoryXMLNode); // Add the index node if required if (contentSpec.getIncludeIndex()) { final Element indexNode = bookBase.createElement("index"); bookBase.getDocumentElement().appendChild(indexNode); } // Change the DOM Document into a string so it can be added to the ZIP final String rootElementName = contentSpec.getBookType().toString().toLowerCase().replace("-draft", ""); final String book = DocBookBuildUtilities.convertDocumentToDocBookFormattedString( buildData.getDocBookVersion(), bookBase, rootElementName, buildData.getEntityFileName(), getXMLFormatProperties()); addToZip(buildData.getBookLocaleFolder() + buildData.getRootBookFileName() + ".xml", book, buildData); } /** * Builds the basics of a Docbook from the resource files for a specific content specification. * * @param buildData Information and data structures for the build. * @return A Document object to be used in generating the book.xml * @throws BuildProcessingException Thrown if an unexpected error occurs during building. */ protected void buildBookAdditions(final BuildData buildData) throws BuildProcessingException { log.info("\tAdding standard files to Publican ZIP file"); final ContentSpec contentSpec = buildData.getContentSpec(); final Map<String, String> overrides = buildData.getBuildOptions().getOverrides(); final Map<String, byte[]> overrideFiles = buildData.getOverrideFiles(); // Load the templates from the server final String bookInfoTemplate; if (contentSpec.getBookType() == BookType.ARTICLE || contentSpec.getBookType() == BookType.ARTICLE_DRAFT) { bookInfoTemplate = stringConstantProvider .getStringConstant(buildData.getServerEntities().getArticleInfoStringConstantId()).getValue(); } else { bookInfoTemplate = stringConstantProvider .getStringConstant(buildData.getServerEntities().getBookInfoStringConstantId()).getValue(); } // Setup Book_Info.xml buildBookInfoFile(buildData, bookInfoTemplate); // Setup Author_Group.xml if (overrides.containsKey(CSConstants.AUTHOR_GROUP_OVERRIDE) && overrideFiles.containsKey(CSConstants.AUTHOR_GROUP_OVERRIDE)) { // Add the override Author_Group.xml file to the book addToZip(buildData.getBookLocaleFolder() + AUTHOR_GROUP_FILE_NAME, overrideFiles.get(CSConstants.AUTHOR_GROUP_OVERRIDE), buildData); } else if (contentSpec.getAuthorGroup() != null) { final TopicErrorData errorData = buildData.getErrorDatabase() .getErrorData(contentSpec.getAuthorGroup().getTopic()); if (errorData != null && errorData.hasFatalErrors()) { buildAuthorGroup(buildData, contentSpec.getAuthorGroup()); } else { final String authorGroupXML = DocBookBuildUtilities.convertDocumentToDocBookFormattedString( buildData.getDocBookVersion(), contentSpec.getAuthorGroup().getXMLDocument(), "authorgroup", buildData.getEntityFileName(), getXMLFormatProperties()); addToZip(buildData.getBookLocaleFolder() + AUTHOR_GROUP_FILE_NAME, authorGroupXML, buildData); } } else { buildAuthorGroup(buildData); } // Add the Feedback.xml if the override exists and we are using the default preface if (contentSpec.getUseDefaultPreface()) { if (overrides.containsKey(CSConstants.FEEDBACK_OVERRIDE) && overrideFiles.containsKey(CSConstants.FEEDBACK_OVERRIDE)) { // Add the override Feedback.xml file to the book addToZip(buildData.getBookLocaleFolder() + FEEDBACK_FILE_NAME, overrideFiles.get(CSConstants.FEEDBACK_OVERRIDE), buildData); } else if (contentSpec.getFeedback() != null) { final String feedbackXml = DocBookBuildUtilities.convertDocumentToDocBookFormattedString( buildData.getDocBookVersion(), contentSpec.getFeedback().getXMLDocument(), DocBookUtilities.TOPIC_ROOT_NODE_NAME, buildData.getEntityFileName(), getXMLFormatProperties()); // Add the feedback directly to the book addToZip(buildData.getBookLocaleFolder() + FEEDBACK_FILE_NAME, feedbackXml, buildData); } } // Setup Legal_Notice.xml if (contentSpec.getLegalNotice() != null) { final String legalNoticeXML = DocBookBuildUtilities.convertDocumentToDocBookFormattedString( buildData.getDocBookVersion(), contentSpec.getLegalNotice().getXMLDocument(), "legalnotice", buildData.getEntityFileName(), getXMLFormatProperties()); addToZip(buildData.getBookLocaleFolder() + LEGAL_NOTICE_FILE_NAME, legalNoticeXML, buildData); } // Setup Preface.xml buildBookPreface(buildData); // Setup Revision_History.xml buildRevisionHistory(buildData, overrides); // Build the book .ent file final String entFile = buildBookEntityFile(buildData); addToZip(buildData.getBookLocaleFolder() + buildData.getEntityFileName(), entFile, buildData); // Setup the images and files folders addBookBaseFilesAndImages(buildData); } /** * Adds the basic Images and Files to the book that are the minimum requirements to build it. * * @param buildData Information and data structures for the build. */ protected void addBookBaseFilesAndImages(final BuildData buildData) throws BuildProcessingException { final String pressgangWebsiteJS = buildPressGangWebsiteJS(buildData); addToZip(buildData.getBookImagesFolder() + "icon.svg", ResourceUtilities.resourceFileToByteArray("/", "icon.svg"), buildData); addToZip(buildData.getBookFilesFolder() + "pressgang_website.js", pressgangWebsiteJS, buildData); } /** * Build up the pressgang_website.js file to be used in help overlays from the topics used in the build. */ protected String buildPressGangWebsiteJS(final BuildData buildData) throws BuildProcessingException { try { final ContentSpec contentSpec = buildData.getContentSpec(); final StringBuilder retValue = new StringBuilder("pressgang_website_callback([\n"); final JsonStringEncoder encoder = JsonStringEncoder.getInstance(); final List<? extends BaseTopicWrapper<?>> topics = buildData.getBuildDatabase().getAllTopics(); boolean initial = true; for (final BaseTopicWrapper<?> topic : topics) { // Ignore info topics if (topic.hasTag(buildData.getServerEntities().getInfoTagId())) continue; // Insert a newline and comma to separate each array variable only after the first variable has been set if (!initial) { retValue.append(",\n"); } else { initial = false; } // Find the fixed url to use final Integer topicId = topic.getTopicId(); final List<ITopicNode> topicNodes = buildData.getBuildDatabase().getTopicNodesForTopicID(topicId); final SpecTopic specTopic = (SpecTopic) topicNodes.get(0); final String fixedUrl = specTopic.getUniqueLinkId(buildData.isUseFixedUrls()); // Find the new since value to use final List<PropertyTagInTopicWrapper> newSinceProperties = topic .getProperties(buildData.getServerEntities().getPressGangWebsitePropertyTagId()); final List<String> values = new ArrayList<String>(); if (newSinceProperties != null) { for (final PropertyTagInTopicWrapper newSinceProperty : newSinceProperties) { values.add(newSinceProperty.getValue()); } Collections.sort(values); } // Opening brace retValue.append("\t{"); // Data retValue.append("\"topicId\":").append(topicId); retValue.append(",\"target\":\"").append(new String(encoder.quoteAsUTF8(fixedUrl), ENCODING)) .append("\""); retValue.append(",\"title\":\"").append(new String(encoder.quoteAsUTF8(topic.getTitle()), ENCODING)) .append("\""); retValue.append(",\"newSince\":\"") .append(values.isEmpty() ? "" : new String(encoder.quoteAsUTF8(values.get(values.size() - 1)), ENCODING)) .append("\""); // Closing brace retValue.append("}"); } retValue.append("]"); // Add the content spec id retValue.append(", ").append(contentSpec.getId()); // Add the product retValue.append(", \"") .append(new String(encoder.quoteAsUTF8( DocBookBuildUtilities.getKeyValueNodeText(buildData, contentSpec.getProductNode())), ENCODING)) .append("\""); // Add the title retValue.append(", \"") .append(new String(encoder.quoteAsUTF8( DocBookBuildUtilities.getKeyValueNodeText(buildData, contentSpec.getTitleNode())), ENCODING)) .append("\""); // Add the version if (contentSpec.getVersion() != null) { retValue.append(", \"") .append(new String(encoder.quoteAsUTF8( DocBookBuildUtilities.getKeyValueNodeText(buildData, contentSpec.getVersionNode())), ENCODING)) .append("\""); } else { retValue.append(", null"); } retValue.append(");"); return retValue.toString(); } catch (UnsupportedEncodingException e) { throw new BuildProcessingException(e); } } /** * Builds the Book_Info.xml file that is a basic requirement to build the book. * * @param buildData Information and data structures for the build. * @param bookInfoTemplate The Book_Info.xml template to add content to. */ protected void buildBookInfoFile(final BuildData buildData, final String bookInfoTemplate) throws BuildProcessingException { final ContentSpec contentSpec = buildData.getContentSpec(); final Map<String, String> overrides = buildData.getBuildOptions().getOverrides(); String bookInfo = bookInfoTemplate.replaceAll(BuilderConstants.ESCAPED_TITLE_REGEX, buildData.getEscapedBookTitle()); // DocBook 5 changed the name of <articleinfo>/<bookinfo> to just <info> final String rootElementName; if (buildData.getDocBookVersion() == DocBookVersion.DOCBOOK_50) { rootElementName = "info"; bookInfo = bookInfo.replaceAll("<(/)?bookinfo", "<$1info").replaceAll("<(/)?articleinfo", "<$1info"); // Change the corpauthor element to orgname bookInfo = bookInfo.replaceAll("<(/)?corpauthor>", "<$1orgname>"); // change the "id" attribute to "xml:id" bookInfo = bookInfo.replace("id=\"", "xml:id=\""); } else { if (contentSpec.getBookType() == BookType.ARTICLE || contentSpec.getBookType() == BookType.ARTICLE_DRAFT) { rootElementName = "articleinfo"; } else { rootElementName = "bookinfo"; } } // Replace the book info elements with the data from the content spec boolean versionSet = false; boolean editionSet = false; boolean abstractSet = false; boolean subtitleSet = false; for (final org.jboss.pressgang.ccms.contentspec.Node node : contentSpec.getNodes()) { if (node instanceof KeyValueNode) { final KeyValueNode<?> keyValueNode = (KeyValueNode<?>) node; if (!(keyValueNode.getValue() instanceof String)) { continue; } if (CommonConstants.CS_TITLE_TITLE.equalsIgnoreCase(keyValueNode.getKey())) { // Set the book title final String title = DocBookBuildUtilities.getKeyValueNodeText(buildData, keyValueNode); bookInfo = bookInfo.replaceAll(BuilderConstants.TITLE_REGEX, DocBookBuildUtilities.escapeForReplaceAll(DocBookUtilities.escapeForXML(title))); } else if (CommonConstants.CS_SUBTITLE_TITLE.equalsIgnoreCase(keyValueNode.getKey())) { // Set the book subtitle final String subtitle = DocBookBuildUtilities.getKeyValueNodeText(buildData, keyValueNode); bookInfo = bookInfo.replaceAll(BuilderConstants.SUBTITLE_REGEX, contentSpec.getSubtitle() == null ? BuilderConstants.SUBTITLE_DEFAULT : DocBookBuildUtilities .escapeForReplaceAll(DocBookUtilities.escapeForXML(subtitle))); subtitleSet = true; } else if (CommonConstants.CS_PRODUCT_TITLE.equalsIgnoreCase(keyValueNode.getKey())) { // Set the book product final String product = DocBookBuildUtilities.getKeyValueNodeText(buildData, keyValueNode); bookInfo = bookInfo.replaceAll(BuilderConstants.PRODUCT_REGEX, DocBookBuildUtilities.escapeForReplaceAll(DocBookUtilities.escapeForXML(product))); } else if (CommonConstants.CS_VERSION_TITLE.equalsIgnoreCase(keyValueNode.getKey())) { // Set the book version final String version = DocBookBuildUtilities.getKeyValueNodeText(buildData, keyValueNode); bookInfo = bookInfo.replaceAll(BuilderConstants.VERSION_REGEX, version == null ? "" : version); versionSet = true; } else if (CommonConstants.CS_EDITION_TITLE.equalsIgnoreCase(keyValueNode.getKey())) { // Set the book edition final String edition = DocBookBuildUtilities.getKeyValueNodeText(buildData, keyValueNode); bookInfo = bookInfo.replaceAll(BuilderConstants.EDITION_REGEX, edition); } else if (CommonConstants.CS_ABSTRACT_TITLE.equalsIgnoreCase(keyValueNode.getKey())) { final String description = DocBookBuildUtilities.getKeyValueNodeText(buildData, keyValueNode); final String fixedAbstract; if (description.matches("^<(formal|sim)?para>(.|\\s)*")) { fixedAbstract = "<abstract>\n\t\t" + description + "\n\t</abstract>\n"; } else { fixedAbstract = "<abstract>\n\t\t<para>\n\t\t\t" + description + "\n\t\t</para>\n\t</abstract>\n"; } // Set the book abstract bookInfo = bookInfo.replaceAll(BuilderConstants.ABSTRACT_REGEX, DocBookBuildUtilities .escapeForReplaceAll(DocBookUtilities.escapeForXML(fixedAbstract))); abstractSet = true; } } } // Apply the null value defaults for version, edition and abstract if (!subtitleSet) { bookInfo = bookInfo.replaceAll(BuilderConstants.SUBTITLE_REGEX, BuilderConstants.SUBTITLE_DEFAULT); } if (!versionSet) { bookInfo = bookInfo.replaceAll(BuilderConstants.VERSION_REGEX, ""); } if (!editionSet) { bookInfo = bookInfo.replaceAll("<edition>.*</edition>(\r)?\n", ""); } if (!abstractSet) { final String fixedAbstract; if (contentSpec.getAbstractTopic() != null) { fixedAbstract = DocBookBuildUtilities.convertDocumentToFormattedString( contentSpec.getAbstractTopic().getXMLDocument(), getXMLFormatProperties()); } else { fixedAbstract = BuilderConstants.DEFAULT_ABSTRACT; } bookInfo = bookInfo.replaceAll(BuilderConstants.ABSTRACT_REGEX, DocBookBuildUtilities.escapeForReplaceAll(fixedAbstract)); } // Get the pubsnumber final String pubsNumber = overrides.containsKey(CSConstants.PUBSNUMBER_OVERRIDE) ? overrides.get(CSConstants.PUBSNUMBER_OVERRIDE) : (contentSpec.getPubsNumber() == null ? BuilderConstants.DEFAULT_PUBSNUMBER : contentSpec.getPubsNumber().toString()); // pubsnumber is different is docbook 5 if (buildData.getDocBookVersion() == DocBookVersion.DOCBOOK_50) { bookInfo = bookInfo.replaceAll(BuilderConstants.PUBSNUMBER_REGEX, "<biblioid class=\"pubsnumber\">" + pubsNumber + "</biblioid>"); } else { bookInfo = bookInfo.replaceAll(BuilderConstants.PUBSNUMBER_REGEX, "<pubsnumber>" + pubsNumber + "</pubsnumber>"); } // Set the book to have a Legal Notice bookInfo = bookInfo.replaceAll(BuilderConstants.LEGAL_NOTICE_REGEX, BuilderConstants.LEGAL_NOTICE_XML); if (!isNullOrEmpty(contentSpec.getBrandLogo())) { final String fixedLogoPath = contentSpec.getBrandLogo().contains("Common_Content/images/") ? contentSpec.getBrandLogo() : ("Common_Content/images/" + contentSpec.getBrandLogo()); bookInfo = bookInfo.replaceFirst("<imagedata.*?/>", "<imagedata fileref=\"" + fixedLogoPath + "\" />"); } // Add the preamble to the book info bookInfo = DocBookBuildUtilities.addDocBookPreamble(buildData.getDocBookVersion(), bookInfo, rootElementName, buildData.getEntityFileName()); if (contentSpec.getBookType() == BookType.ARTICLE || contentSpec.getBookType() == BookType.ARTICLE_DRAFT) { addToZip(buildData.getBookLocaleFolder() + "Article_Info.xml", bookInfo, buildData); } else { addToZip(buildData.getBookLocaleFolder() + "Book_Info.xml", bookInfo, buildData); } } /** * Builds the Preface.xml file for the book. * * @param buildData * @throws BuildProcessingException */ protected void buildBookPreface(final BuildData buildData) throws BuildProcessingException { final ContentSpec contentSpec = buildData.getContentSpec(); // Check that should actually use the preface if (contentSpec.getUseDefaultPreface()) { final Map<String, String> overrides = buildData.getBuildOptions().getOverrides(); final Map<String, byte[]> overrideFiles = buildData.getOverrideFiles(); final Document prefaceDoc; try { prefaceDoc = XMLUtilities.convertStringToDocument("<preface></preface>"); } catch (Exception e) { throw new BuildProcessingException(e); } // Add the title final String prefaceTitleTranslation = buildData.getConstants().getString("PREFACE"); final Element titleEle = prefaceDoc.createElement("title"); titleEle.setTextContent(prefaceTitleTranslation); prefaceDoc.getDocumentElement().appendChild(titleEle); // Add the Conventions.xml final Element conventions = XMLUtilities.createXIInclude(prefaceDoc, "Common_Content/Conventions.xml"); prefaceDoc.getDocumentElement().appendChild(conventions); // Add the Feedback.xml if (overrides.containsKey(CSConstants.FEEDBACK_OVERRIDE) && overrideFiles.containsKey(CSConstants.FEEDBACK_OVERRIDE) || contentSpec.getFeedback() != null) { final Element xinclude = XMLUtilities.createXIInclude(prefaceDoc, "Feedback.xml"); prefaceDoc.getDocumentElement().appendChild(xinclude); } else { final Element xinclude = XMLUtilities.createXIInclude(prefaceDoc, "Common_Content/Feedback.xml"); prefaceDoc.getDocumentElement().appendChild(xinclude); } final String prefaceXml = DocBookBuildUtilities.convertDocumentToDocBookFormattedString( buildData.getDocBookVersion(), prefaceDoc, "preface", buildData.getEntityFileName(), getXMLFormatProperties()); addToZip(buildData.getBookLocaleFolder() + PREFACE_FILE_NAME, prefaceXml, buildData); } } /** * Builds the book .ent file that is a basic requirement to build the book. * * @param buildData Information and data structures for the build. * @return The book .ent file filled with content from the Content Spec. */ protected String buildBookEntityFile(final BuildData buildData) throws BuildProcessingException { final ContentSpec contentSpec = buildData.getContentSpec(); final StringBuilder retValue = new StringBuilder(); if (!buildData.getBuildOptions().getUseOldBugLinks() && buildData.getBuildOptions().getInsertBugLinks()) { // Add the bug link entities retValue.append("<!-- BUG LINK ENTITIES -->\n"); try { final BaseBugLinkStrategy bugLinkStrategy = buildData.getBugLinkStrategy(); final BugLinkOptions bugLinkOptions = buildData.getBugLinkOptions(); retValue.append(bugLinkStrategy.generateEntities(bugLinkOptions, buildData.getBuildName(), buildData.getBuildDate())); } catch (UnsupportedEncodingException e) { throw new BuildProcessingException(e); } catch (BugLinkException e) { throw new BuildProcessingException(e); } catch (final Exception ex) { throw new BuildProcessingException("Failed to insert Bug Links into the DOM Document", ex); } } retValue.append("<!-- CS ENTITIES -->\n"); final String entities = ContentSpecUtilities.generateEntitiesForContentSpec(contentSpec, buildData.getDocBookVersion(), buildData.getEscapedBookTitle(), buildData.getOriginalBookTitle(), buildData.getOriginalBookProduct()); retValue.append(entities); // Add the docbook.ent file for DocBook 5 builds if (buildData.getDocBookVersion() == DocBookVersion.DOCBOOK_50) { retValue.append("<!-- START DOCBOOK ENTITIES -->\n"); retValue.append(docbook45Entities); } return retValue.toString(); } /** * Creates all the chapters/appendixes for a book and generates the section/topic data inside of each chapter. * * @param buildData Information and data structures for the build. * @param doc The Book document object to add the child level content to. * @param level The level to build the chapter from. * @param flattenStructure Whether or not the build should be flattened. * @return The Element that specifies the XiInclude for the chapter/appendix in the files. * @throws BuildProcessingException Thrown if an unexpected error occurs during building. */ protected Element createRootContainerXML(final BuildData buildData, final Document doc, final Level level, final boolean flattenStructure) throws BuildProcessingException { // Check if the app should be shutdown if (isShuttingDown.get()) { return null; } // Get the name of the element based on the type final String elementName = level.getLevelType() == LevelType.PROCESS ? "chapter" : level.getLevelType().getTitle().toLowerCase(Locale.ENGLISH); Document container = null; try { container = XMLUtilities.convertStringToDocument("<" + elementName + "></" + elementName + ">"); } catch (Exception ex) { // Exit since we shouldn't fail at converting a basic chapter log.debug("", ex); throw new BuildProcessingException(getMessages().getString("FAILED_CREATING_BASIC_TEMPLATE")); } // Create the title final String containerName = level.getUniqueLinkId(buildData.isUseFixedUrls()); final String containerXMLName = containerName + ".xml"; // Create the xiInclude to be added to the book.xml file final Element xiInclude = XMLUtilities.createXIInclude(doc, containerXMLName); // Setup the title and id setUpRootElement(buildData, level, container, container.getDocumentElement()); // Create and add the chapter/level contents createContainerXML(buildData, level, container, container.getDocumentElement(), buildData.getBookTopicsFolder() + containerName + "/", flattenStructure); // Add the boiler plate text and add the chapter to the book final String chapterString = DocBookBuildUtilities.convertDocumentToDocBookFormattedString( buildData.getDocBookVersion(), container, elementName, buildData.getEntityFileName(), getXMLFormatProperties()); addToZip(buildData.getBookLocaleFolder() + containerXMLName, chapterString, buildData); return xiInclude; } /** * Creates all the chapters/appendixes for a book that are contained within another part/chapter/appendix and generates the * section/topic data inside of each chapter. * * @param buildData Information and data structures for the build. * @param doc The document object to add the child level content to. * @param container The level to build the chapter from. * @param parentFileDirectory The parent file location, so any files can be saved in a subdirectory of the parents location. * @param flattenStructure Whether or not the build should be flattened. * @return The Element that specifies the XiInclude for the chapter/appendix in the files. * @throws BuildProcessingException Thrown if an unexpected error occurs during building. */ protected Element createSubRootContainerXML(final BuildData buildData, final Document doc, final Level container, final String parentFileDirectory, final boolean flattenStructure) throws BuildProcessingException { // Check if the app should be shutdown if (isShuttingDown.get()) { return null; } // Get the name of the element based on the type final String elementName = container.getLevelType() == LevelType.PROCESS ? "chapter" : container.getLevelType().getTitle().toLowerCase(Locale.ENGLISH); Document chapter = null; try { chapter = XMLUtilities.convertStringToDocument("<" + elementName + "></" + elementName + ">"); } catch (Exception ex) { // Exit since we shouldn't fail at converting a basic chapter log.debug("", ex); throw new BuildProcessingException(getMessages().getString("FAILED_CREATING_BASIC_TEMPLATE")); } // Create the title final String chapterName = container.getUniqueLinkId(buildData.isUseFixedUrls()); final String chapterXMLName = chapterName + ".xml"; // Setup the title and id setUpRootElement(buildData, container, chapter, chapter.getDocumentElement()); // Create and add the chapter/level contents createContainerXML(buildData, container, chapter, chapter.getDocumentElement(), parentFileDirectory + chapterName + "/", flattenStructure); // Add the boiler plate text and add the chapter to the book final String chapterString = DocBookBuildUtilities.convertDocumentToDocBookFormattedString( buildData.getDocBookVersion(), chapter, elementName, buildData.getEntityFileName(), getXMLFormatProperties()); addToZip(buildData.getBookLocaleFolder() + chapterXMLName, chapterString, buildData); // Create the XIncludes that will get set in the book.xml final Element xiInclude = XMLUtilities.createXIInclude(doc, chapterXMLName); return xiInclude; } /** * Sets up an elements title, info and id based on the passed level. * * @param buildData Information and data structures for the build. * @param level The level to build the root element is being built for. * @param doc The document object the content is being added to. * @param ele */ protected void setUpRootElement(final BuildData buildData, final Level level, final Document doc, Element ele) { final Element titleNode = doc.createElement("title"); if (buildData.isTranslationBuild() && !isNullOrEmpty(level.getTranslatedTitle())) { titleNode.setTextContent(DocBookUtilities.escapeForXML(level.getTranslatedTitle())); } else { titleNode.setTextContent(DocBookUtilities.escapeForXML(level.getTitle())); } // Add the info if the container has one final Element infoElement; if (level.getInfoTopic() != null) { final InfoTopic infoTopic = level.getInfoTopic(); final Node info = doc.importNode(infoTopic.getXMLDocument().getDocumentElement(), true); // Generate the info node final String elementInfoName; if (buildData.getDocBookVersion() == DocBookVersion.DOCBOOK_50) { elementInfoName = "info"; } else { elementInfoName = ele.getNodeName() + "info"; } infoElement = doc.createElement(elementInfoName); // Move the contents of the info to the chapter/level final NodeList infoChildren = info.getChildNodes(); while (infoChildren.getLength() > 0) { infoElement.appendChild(infoChildren.item(0)); } } else { infoElement = null; } if (buildData.getDocBookVersion() == DocBookVersion.DOCBOOK_50) { ele.appendChild(titleNode); if (infoElement != null) { ele.appendChild(infoElement); } } else { if (infoElement != null) { ele.appendChild(infoElement); } ele.appendChild(titleNode); } DocBookBuildUtilities.setDOMElementId(buildData.getDocBookVersion(), ele, level.getUniqueLinkId(buildData.isUseFixedUrls())); } /** * Creates the section component of a chapter.xml for a specific Level. * * @param buildData Information and data structures for the build. * @param container The level object to get content from. * @param doc The document object that this container is to be added to. * @param parentNode The parent XML node of this section. * @param parentFileLocation The parent file location, so any files can be saved in a subdirectory of the parents location. * @param flattenStructure Whether or not the build should be flattened. * @throws BuildProcessingException Thrown if an unexpected error occurs during building. */ protected void createContainerXML(final BuildData buildData, final Level container, final Document doc, final Element parentNode, final String parentFileLocation, final Boolean flattenStructure) throws BuildProcessingException { final List<org.jboss.pressgang.ccms.contentspec.Node> levelData = container.getChildNodes(); // Get the name of the element based on the type final String elementName = container.getLevelType() == LevelType.PROCESS ? "chapter" : container.getLevelType().getTitle().toLowerCase(Locale.ENGLISH); final Element intro = doc.createElement(elementName + "intro"); // Storage container to hold the levels so they can be added in proper order with the intro final LinkedList<Node> childNodes = new LinkedList<Node>(); // Add the section and topics for this level to the chapter.xml for (final org.jboss.pressgang.ccms.contentspec.Node node : levelData) { // Check if the app should be shutdown if (isShuttingDown.get()) { return; } if (node instanceof Level && node.getParent() != null && (((Level) node).getParent().getLevelType() == LevelType.BASE || ((Level) node).getParent().getLevelType() == LevelType.PART) && ((Level) node).getLevelType() != LevelType.INITIAL_CONTENT) { final Level childContainer = (Level) node; // Create a new file for the Chapter/Appendix final Element xiInclude = createSubRootContainerXML(buildData, doc, childContainer, parentFileLocation, flattenStructure); if (xiInclude != null) { childNodes.add(xiInclude); } } else if (node instanceof Level && ((Level) node).getLevelType() == LevelType.INITIAL_CONTENT) { if (container.getLevelType() == LevelType.PART) { addLevelsInitialContent(buildData, (InitialContent) node, doc, intro, false); } else { addLevelsInitialContent(buildData, (InitialContent) node, doc, parentNode, false); } } else if (node instanceof Level) { final Level childLevel = (Level) node; // Create the section and its title final Element sectionNode = doc.createElement("section"); setUpRootElement(buildData, childLevel, doc, sectionNode); // Ignore sections that have no spec topics if (!childLevel.hasSpecTopics() && !childLevel.hasCommonContents()) { if (buildData.getBuildOptions().isAllowEmptySections()) { Element warning = doc.createElement("warning"); warning.setTextContent("No Content"); sectionNode.appendChild(warning); } else { continue; } } else { // Add this sections child sections/topics createContainerXML(buildData, childLevel, doc, sectionNode, parentFileLocation, flattenStructure); } childNodes.add(sectionNode); } else if (node instanceof CommonContent) { final CommonContent commonContent = (CommonContent) node; final Node xiInclude = XMLUtilities.createXIInclude(doc, "Common_Content/" + commonContent.getFixedTitle()); if (commonContent.getParent() != null && commonContent.getParent().getLevelType() == LevelType.PART) { intro.appendChild(xiInclude); } else { childNodes.add(xiInclude); } } else if (node instanceof SpecTopic) { final SpecTopic specTopic = (SpecTopic) node; final Node topicNode = createTopicDOMNode(specTopic, doc, flattenStructure, parentFileLocation); // Add the node to the chapter if (topicNode != null) { if (specTopic.getParent() != null && ((Level) specTopic.getParent()).getLevelType() == LevelType.PART) { intro.appendChild(topicNode); } else { childNodes.add(topicNode); } } } } // Add the child nodes and intro to the parent if (intro.hasChildNodes()) { parentNode.appendChild(intro); } for (final Node node : childNodes) { parentNode.appendChild(node); } } protected void addLevelsInitialContent(final BuildData buildData, final InitialContent initialContent, final Document chapter, final Element parentNode) throws BuildProcessingException { addLevelsInitialContent(buildData, initialContent, chapter, parentNode, true); } protected void addLevelsInitialContent(final BuildData buildData, final InitialContent initialContent, final Document chapter, final Element parentNode, final boolean includeInfo) throws BuildProcessingException { // Copy the body content of the topics to the level's front matter boolean containsCommonContent = false; for (final org.jboss.pressgang.ccms.contentspec.Node initialContentNode : initialContent.getChildNodes()) { if (initialContentNode instanceof CommonContent) { final Node node = XMLUtilities.createXIInclude(chapter, "Common_Content/" + ((CommonContent) initialContentNode).getFixedTitle()); parentNode.appendChild(node); containsCommonContent = true; } else if (initialContentNode instanceof SpecTopic) { // Insert the topic DOM document into the parent document addTopicContentsToLevelDocument(buildData.getDocBookVersion(), initialContent, (SpecTopic) initialContentNode, parentNode, chapter, includeInfo); } } final DocBookXMLPreProcessor xmlPreProcessor = buildData.getXMLPreProcessor(); // Process the see also/prereq injections for the level processLevelInjections(buildData, initialContent, chapter, parentNode, xmlPreProcessor); // Add the bug links for the front matter content if it has no common content if (!containsCommonContent) { xmlPreProcessor.processInitialContentBugLink(buildData, initialContent, chapter, parentNode); } } /** * Adds a Topics contents as the introduction text for a Level. * * @param docBookVersion * @param level The level the intro topic is being added for. * @param specTopic The Topic that contains the introduction content. * @param parentNode The DOM parent node the intro content is to be appended to. * @param doc The DOM Document the content is to be added to. */ protected void addTopicContentsToLevelDocument(final DocBookVersion docBookVersion, final Level level, final SpecTopic specTopic, final Element parentNode, final Document doc) { addTopicContentsToLevelDocument(docBookVersion, level, specTopic, parentNode, doc, true); } /** * Adds a Topics contents as the introduction text for a Level. * * @param docBookVersion * @param level The level the intro topic is being added for. * @param specTopic The Topic that contains the introduction content. * @param parentNode The DOM parent node the intro content is to be appended to. * @param doc The DOM Document the content is to be added to. */ protected void addTopicContentsToLevelDocument(final DocBookVersion docBookVersion, final Level level, final SpecTopic specTopic, final Element parentNode, final Document doc, final boolean includeInfo) { final Node section = doc.importNode(specTopic.getXMLDocument().getDocumentElement(), true); final String infoName; if (docBookVersion == DocBookVersion.DOCBOOK_50) { infoName = "info"; } else { infoName = DocBookUtilities.TOPIC_ROOT_SECTIONINFO_NODE_NAME; } if (includeInfo && (level.getLevelType() != LevelType.PART)) { // Reposition the sectioninfo final List<Node> sectionInfoNodes = XMLUtilities.getDirectChildNodes(section, infoName); if (sectionInfoNodes.size() != 0) { final String parentInfoName; if (docBookVersion == DocBookVersion.DOCBOOK_50) { parentInfoName = "info"; } else { parentInfoName = parentNode.getNodeName() + "info"; } // Check if the parent already has a info node final List<Node> infoNodes = XMLUtilities.getDirectChildNodes(parentNode, parentInfoName); final Node infoNode; if (infoNodes.size() == 0) { infoNode = doc.createElement(parentInfoName); DocBookUtilities.setInfo(docBookVersion, (Element) infoNode, parentNode); } else { infoNode = infoNodes.get(0); } // Merge the info text final NodeList sectionInfoChildren = sectionInfoNodes.get(0).getChildNodes(); final Node firstNode = infoNode.getFirstChild(); while (sectionInfoChildren.getLength() > 0) { if (firstNode != null) { infoNode.insertBefore(sectionInfoChildren.item(0), firstNode); } else { infoNode.appendChild(sectionInfoChildren.item(0)); } } } } // Remove the title and sectioninfo final List<Node> titleNodes = XMLUtilities.getDirectChildNodes(section, DocBookUtilities.TOPIC_ROOT_TITLE_NODE_NAME, infoName); for (final Node removeNode : titleNodes) { section.removeChild(removeNode); } // Move the contents of the section to the chapter/level final NodeList sectionChildren = section.getChildNodes(); while (sectionChildren.getLength() > 0) { parentNode.appendChild(sectionChildren.item(0)); } } protected Node createTopicDOMNode(final SpecTopic specTopic, final Document doc, final boolean flattenStructure, final String parentFileLocation) throws BuildProcessingException { final Document topicDoc = specTopic.getXMLDocument(); final Node topicNode; if (flattenStructure) { // Include the topic as is, into the chapter topicNode = doc.importNode(topicDoc.getDocumentElement(), true); } else { // Create the topic file and add the reference to the chapter final String topicFileName = createTopicXMLFile(buildData, specTopic, parentFileLocation); if (topicFileName != null) { // Remove the initial file location as we only want where it lives in the topics directory final String fixedParentFileLocation = buildData.getBuildOptions().getFlattenTopics() ? "topics/" : parentFileLocation.replace(buildData.getBookLocaleFolder(), ""); topicNode = XMLUtilities.createXIInclude(doc, fixedParentFileLocation + topicFileName); } else { topicNode = null; } } return topicNode; } /** * Creates the Topic component of a chapter.xml for a specific SpecTopic. * * @param buildData Information and data structures for the build. * @param specTopic The build topic object to get content from. * @param parentFileLocation The topics parent file location, so the topic can be saved in a subdirectory. * @return The Topics filename. */ protected String createTopicXMLFile(final BuildData buildData, final SpecTopic specTopic, final String parentFileLocation) throws BuildProcessingException { String topicFileName; final BaseTopicWrapper<?> topic = specTopic.getTopic(); if (topic != null) { topicFileName = specTopic.getUniqueLinkId(buildData.isUseFixedUrls()) + ".xml"; final String fixedParentFileLocation = buildData.getBuildOptions().getFlattenTopics() ? buildData.getBookTopicsFolder() : parentFileLocation; final String fixedEntityPath = fixedParentFileLocation.replace(buildData.getBookLocaleFolder(), "") .replaceAll(".*?" + File.separator + "", "../"); final String topicXML = DocBookBuildUtilities.convertDocumentToDocBookFormattedString( buildData.getDocBookVersion(), specTopic.getXMLDocument(), DocBookUtilities.TOPIC_ROOT_NODE_NAME, fixedEntityPath + buildData.getEntityFileName(), getXMLFormatProperties()); addToZip(fixedParentFileLocation + topicFileName, topicXML, buildData); return topicFileName; } return null; } private void addImagesToBook(final BuildData buildData) { addImagesToBook(buildData, buildData.getBuildLocale(), buildData.getBookLocaleFolder(), true, true); } /** * Adds all the images found using the {@link #processImageLocations(BuildData)} method to the files map that will alter be turned * into a ZIP archive. * * @param buildData Information and data structures for the build. */ protected void addImagesToBook(final BuildData buildData, final String locale, final String imageFolder, boolean revertToDefaultLocale, boolean logErrors) { // Load the database constants final byte[] failpenguinPng = blobConstantProvider .getBlobConstant(buildData.getServerEntities().getFailPenguinBlobConstantId()).getValue(); // Download the image files that were identified in the processing stage float imageProgress = 0; final float imageTotal = buildData.getImageLocations().size(); final int showPercent = 10; int lastPercent = 0; for (final TopicImageData imageLocation : buildData.getImageLocations()) { // Check if the app should be shutdown if (isShuttingDown.get()) { return; } boolean success = false; final int extensionIndex = imageLocation.getImageName().lastIndexOf("."); final int pathIndex = imageLocation.getImageName().lastIndexOf("/"); final int hypenIndex = imageLocation.getImageName().lastIndexOf("-"); if ( /* characters were found */ extensionIndex != -1 && pathIndex != -1 /* the path character was found before the extension */ && extensionIndex > pathIndex) { try { /* * The file name minus the extension should be an integer that references an ImageFile record ID. */ final String imageID; if (hypenIndex != -1) { imageID = imageLocation.getImageName().substring(pathIndex + 1, Math.min(extensionIndex, hypenIndex)); } else { imageID = imageLocation.getImageName().substring(pathIndex + 1, extensionIndex); } /* * If the image is the failpenguin the that means that an error has already occurred most likely from not * specifying an image file at all. */ if (imageID.equals(BuilderConstants.FAILPENGUIN_PNG_NAME)) { success = false; buildData.getErrorDatabase().addError(imageLocation.getTopic(), ErrorType.INVALID_IMAGES, "No image filename specified. Must be in the format [ImageFileID].extension e.g. 123.png, " + "" + "or images/321.jpg"); } else { final ImageWrapper imageFile = imageProvider.getImage(Integer.parseInt(imageID)); // TODO Uncomment this once Image Revisions are fixed. // if (imageLocation.getRevision() == null) { // imageFile = imageProvider.getImage(Integer.parseInt(imageID)); // } else { // imageFile = imageProvider.getImage(Integer.parseInt(imageID), imageLocation.getRevision()); // } // Find the image that matches this locale. If the locale isn't found then use the default locale LanguageImageWrapper languageImageFile = null; if (imageFile.getLanguageImages() != null && imageFile.getLanguageImages().getItems() != null) { final List<LanguageImageWrapper> languageImages = imageFile.getLanguageImages() .getItems(); for (final LanguageImageWrapper image : languageImages) { if (image.getLocale().getValue().equals(locale)) { languageImageFile = image; break; } else if (revertToDefaultLocale && image.getLocale().getValue().equals(buildData.getDefaultLocale()) && languageImageFile == null) { languageImageFile = image; } } } if (languageImageFile != null && languageImageFile.getImageData() != null) { success = true; addToZip(buildData.getBookLocaleFolder() + imageLocation.getImageName(), languageImageFile.getImageData(), buildData); } else if (logErrors) { buildData.getErrorDatabase().addError(imageLocation.getTopic(), ErrorType.INVALID_IMAGES, "ImageFile ID " + imageID + " from image location " + imageLocation.getImageName() + " was not found!"); } } } catch (final NumberFormatException ex) { success = false; if (logErrors) { buildData.getErrorDatabase().addError(imageLocation.getTopic(), ErrorType.INVALID_IMAGES, imageLocation.getImageName() + " is not a valid image. Must be in the format [ImageFileID].extension e.g." + " 123" + ".png, or images/321.jpg"); log.debug("", ex); } } catch (final Exception ex) { success = false; if (logErrors) { buildData.getErrorDatabase().addError(imageLocation.getTopic(), ErrorType.INVALID_IMAGES, imageLocation.getImageName() + " is not a valid image. Must be in the format [ImageFileID].extension e.g." + " 123" + ".png, or images/321.jpg"); log.debug("", ex); } } } // Put in a place holder in the image couldn't be found if (logErrors && !success) { buildData.getOutputFiles().put(imageFolder + imageLocation.getImageName(), failpenguinPng); } final int progress = Math.round(imageProgress / imageTotal * 100); if (progress - lastPercent >= showPercent) { lastPercent = progress; log.info("\tDownloading " + locale + " Images " + progress + "% done"); } ++imageProgress; } } /** * Builds the Author_Group.xml using the assigned writers for topics inside of the content specification. * * @param buildData Information and data structures for the build. * @throws BuildProcessingException */ private void buildAuthorGroup(final BuildData buildData) throws BuildProcessingException { buildAuthorGroup(buildData, null); } /** * Builds the Author_Group.xml using the assigned writers for topics inside of the content specification. * * @param buildData Information and data structures for the build. * @param specTopic The topic to build the Author Group in place of. * @throws BuildProcessingException */ private void buildAuthorGroup(final BuildData buildData, final SpecTopic specTopic) throws BuildProcessingException { log.info("\tBuilding " + AUTHOR_GROUP_FILE_NAME); // Setup Author_Group.xml Document authorDoc = null; try { authorDoc = XMLUtilities.convertStringToDocument("<authorgroup></authorgroup>"); } catch (Exception ex) { // Exit since we shouldn't fail at converting the basic author group log.debug("", ex); throw new BuildProcessingException( String.format(getMessages().getString("FAILED_CONVERTING_TEMPLATE"), AUTHOR_GROUP_FILE_NAME)); } final LinkedHashMap<Integer, AuthorInformation> authorIDtoAuthor = new LinkedHashMap<Integer, AuthorInformation>(); // Check if the app should be shutdown if (isShuttingDown.get()) { return; } // Set the id if (specTopic != null) { DocBookBuildUtilities.processTopicID(buildData, specTopic, authorDoc); } else { DocBookBuildUtilities.setDOMElementId(buildData.getDocBookVersion(), authorDoc.getDocumentElement(), "Author_Group"); } // Get the mapping of authors using the topics inside the content spec for (final Integer topicId : buildData.getBuildDatabase().getTopicIds()) { final BaseTopicWrapper<?> topic = buildData.getBuildDatabase().getTopicNodesForTopicID(topicId).get(0) .getTopic(); final List<TagWrapper> authorTags = topic.getTagsInCategories( CollectionUtilities.toArrayList(buildData.getServerEntities().getWriterCategoryId())); if (authorTags.size() > 0) { for (final TagWrapper author : authorTags) { if (!authorIDtoAuthor.containsKey(author.getId())) { final AuthorInformation authorInfo = EntityUtilities.getAuthorInformation(providerFactory, buildData.getServerEntities(), author.getId(), author.getRevision()); if (authorInfo != null) { authorIDtoAuthor.put(author.getId(), authorInfo); } } } } } // Sort and make sure duplicate authors don't exist final Set<AuthorInformation> authors = new TreeSet<AuthorInformation>(new AuthorInformationComparator()); for (final Entry<Integer, AuthorInformation> authorEntry : authorIDtoAuthor.entrySet()) { final AuthorInformation authorInfo = authorEntry.getValue(); if (authorInfo != null) { authors.add(authorInfo); } } // Check if the app should be shutdown if (isShuttingDown.get()) { return; } boolean insertedAuthor = false; // If one or more authors were found then remove the default and attempt to add them if (!authors.isEmpty()) { // For each author attempt to find the author information records and populate Author_Group.xml. for (final AuthorInformation authorInfo : authors) { // Check if the app should be shutdown if (isShuttingDown.get()) { shutdown.set(true); return; } final Element authorEle = authorDoc.createElement("author"); final Element firstNameEle = authorDoc.createElement("firstname"); firstNameEle.setTextContent(authorInfo.getFirstName()); final Element lastNameEle = authorDoc.createElement("surname"); lastNameEle.setTextContent(authorInfo.getLastName()); // Docbook 5 needs <firstname>/<surname> wrapped in <personname> if (buildData.getDocBookVersion() == DocBookVersion.DOCBOOK_50) { final Element personnameEle = authorDoc.createElement("personname"); authorEle.appendChild(personnameEle); personnameEle.appendChild(firstNameEle); personnameEle.appendChild(lastNameEle); } else { authorEle.appendChild(firstNameEle); authorEle.appendChild(lastNameEle); } // Add the affiliation information if (authorInfo.getOrganization() != null) { final Element affiliationEle = authorDoc.createElement("affiliation"); final Element orgEle = authorDoc.createElement("orgname"); orgEle.setTextContent(authorInfo.getOrganization()); affiliationEle.appendChild(orgEle); if (authorInfo.getOrgDivision() != null) { final Element orgDivisionEle = authorDoc.createElement("orgdiv"); orgDivisionEle.setTextContent(authorInfo.getOrgDivision()); affiliationEle.appendChild(orgDivisionEle); } authorEle.appendChild(affiliationEle); } // Add an email if one exists if (authorInfo.getEmail() != null) { Element emailEle = authorDoc.createElement("email"); emailEle.setTextContent(authorInfo.getEmail()); authorEle.appendChild(emailEle); } authorDoc.getDocumentElement().appendChild(authorEle); insertedAuthor = true; } } // If no authors were inserted then use a default value if (!insertedAuthor) { // Use the author "PressGang CCMS Build System" final Element authorEle = authorDoc.createElement("author"); authorDoc.getDocumentElement().appendChild(authorEle); // Use the author "PressGang Alpha Build System" final Element firstNameEle = authorDoc.createElement("firstname"); firstNameEle.setTextContent(""); final Element lastNameEle = authorDoc.createElement("surname"); lastNameEle.setTextContent("PressGang CCMS Build System"); // Docbook 5 needs <firstname>/<surname> wrapped in <personname> if (buildData.getDocBookVersion() == DocBookVersion.DOCBOOK_50) { final Element personnameEle = authorDoc.createElement("personname"); authorEle.appendChild(personnameEle); personnameEle.appendChild(firstNameEle); personnameEle.appendChild(lastNameEle); } else { authorEle.appendChild(firstNameEle); authorEle.appendChild(lastNameEle); } // Add the affiliation final Element affiliationEle = authorDoc.createElement("affiliation"); final Element orgEle = authorDoc.createElement("orgname"); orgEle.setTextContent("Red Hat"); affiliationEle.appendChild(orgEle); final Element orgDivisionEle = authorDoc.createElement("orgdiv"); orgDivisionEle.setTextContent("Engineering Content Services"); affiliationEle.appendChild(orgDivisionEle); authorEle.appendChild(affiliationEle); } // Add the Author_Group.xml to the book final String authorGroupXml = DocBookBuildUtilities.convertDocumentToDocBookFormattedString( buildData.getDocBookVersion(), authorDoc, "authorgroup", buildData.getEntityFileName(), getXMLFormatProperties()); addToZip(buildData.getBookLocaleFolder() + AUTHOR_GROUP_FILE_NAME, authorGroupXml, buildData); } /** * Builds the revision history for the book. The revision history used will be determined in the following order:<br/> * <br /> * 1. Revision History Override<br/> * 2. Content Spec Revision History Topic<br/> * 3. Revision History Template * * @param buildData Information and data structures for the build. * @param overrides The overrides to use for the build. * @throws BuildProcessingException */ protected void buildRevisionHistory(final BuildData buildData, final Map<String, String> overrides) throws BuildProcessingException { final ContentSpec contentSpec = buildData.getContentSpec(); // Replace the basic injection data inside the revision history final String revisionHistoryXml = stringConstantProvider .getStringConstant(buildData.getServerEntities().getRevisionHistoryStringConstantId()).getValue(); // DocBook 5 shouldn't have the <revhistory> wrapped in a <simpara> final String fixedRevisionHistoryXml; if (buildData.getDocBookVersion() == DocBookVersion.DOCBOOK_50) { fixedRevisionHistoryXml = revisionHistoryXml .replaceAll(BuilderConstants.ESCAPED_TITLE_REGEX, buildData.getEscapedBookTitle()) .replace("<simpara>", "").replace("</simpara>", ""); } else { fixedRevisionHistoryXml = revisionHistoryXml.replaceAll(BuilderConstants.ESCAPED_TITLE_REGEX, buildData.getEscapedBookTitle()); } // Setup Revision_History.xml if (overrides.containsKey(CSConstants.REVISION_HISTORY_OVERRIDE) && buildData.getOverrideFiles().containsKey(CSConstants.REVISION_HISTORY_OVERRIDE)) { byte[] revHistory = buildData.getOverrideFiles().get(CSConstants.REVISION_HISTORY_OVERRIDE); if (buildData.getBuildOptions().getRevisionMessages() != null && !buildData.getBuildOptions().getRevisionMessages().isEmpty()) { try { // Parse the Revision History and add the new entry/entries final ByteArrayInputStream bais = new ByteArrayInputStream(revHistory); final BufferedReader reader = new BufferedReader(new InputStreamReader(bais)); final StringBuilder buffer = new StringBuilder(); String line = ""; while ((line = reader.readLine()) != null) { buffer.append(line + "\n"); } if (buildData.getBuildOptions().getRevisionMessages() != null && !buildData.getBuildOptions().getRevisionMessages().isEmpty()) { // Add a revision message to the Revision_History.xml final String revHistoryOverride = buffer.toString(); final String docType = XMLUtilities.findDocumentType(revHistoryOverride); if (docType != null) { buildRevisionHistoryFromTemplate(buildData, revHistoryOverride.replace(docType, "")); } else { buildRevisionHistoryFromTemplate(buildData, revHistoryOverride); } } else { addToZip(buildData.getBookLocaleFolder() + "Revision_History.xml", buffer.toString(), buildData); } } catch (Exception e) { log.error(e.getMessage()); buildRevisionHistoryFromTemplate(buildData, fixedRevisionHistoryXml); } } else { // Add the revision history directly to the book buildData.getOutputFiles().put(buildData.getBookLocaleFolder() + REVISION_HISTORY_FILE_NAME, revHistory); } } else if (contentSpec.getRevisionHistory() != null) { final TopicErrorData errorData = buildData.getErrorDatabase() .getErrorData(contentSpec.getRevisionHistory().getTopic()); final String revisionHistoryXML = DocBookBuildUtilities.convertDocumentToDocBookFormattedString( buildData.getDocBookVersion(), contentSpec.getRevisionHistory().getXMLDocument(), "appendix", buildData.getEntityFileName(), getXMLFormatProperties()); if (buildData.getBuildOptions().getRevisionMessages() != null && !buildData.getBuildOptions().getRevisionMessages().isEmpty()) { buildRevisionHistoryFromTemplate(buildData, revisionHistoryXML); } else if (errorData != null && errorData.hasFatalErrors()) { buildRevisionHistoryFromTemplate(buildData, revisionHistoryXML); } else { // Add the revision history directly to the book addToZip(buildData.getBookLocaleFolder() + REVISION_HISTORY_FILE_NAME, revisionHistoryXML, buildData); } } else { buildRevisionHistoryFromTemplate(buildData, fixedRevisionHistoryXml); } } /** * Builds the revision history using the requester of the build. * * @param buildData Information and data structures for the build. * @param revisionHistoryXml The Revision_History.xml file/template to add revision information to. * @throws BuildProcessingException Thrown if an unexpected error occurs during building. */ protected void buildRevisionHistoryFromTemplate(final BuildData buildData, final String revisionHistoryXml) throws BuildProcessingException { log.info("\tBuilding " + REVISION_HISTORY_FILE_NAME); Document revHistoryDoc; try { revHistoryDoc = XMLUtilities.convertStringToDocument(revisionHistoryXml); } catch (Exception ex) { // Exit since we shouldn't fail at converting the basic revision history log.debug("", ex); throw new BuildProcessingException(String.format(getMessages().getString("FAILED_CONVERTING_TEMPLATE"), REVISION_HISTORY_FILE_NAME)); } if (revHistoryDoc == null) { throw new BuildProcessingException(String.format(getMessages().getString("FAILED_CONVERTING_TEMPLATE"), REVISION_HISTORY_FILE_NAME)); } final String reportHistoryTitleTranslation = buildData.getConstants().getString("REVISION_HISTORY"); if (reportHistoryTitleTranslation != null) { DocBookUtilities.setRootElementTitle(reportHistoryTitleTranslation, revHistoryDoc); } // Find the revhistory node final Element revHistory; final NodeList revHistories = revHistoryDoc.getElementsByTagName("revhistory"); if (revHistories.getLength() > 0) { revHistory = (Element) revHistories.item(0); } else { revHistory = revHistoryDoc.createElement("revhistory"); // <revhistory> should be a direct child of <appendix> in docbook 5 if (buildData.getDocBookVersion() == DocBookVersion.DOCBOOK_50) { revHistoryDoc.getDocumentElement().appendChild(revHistory); } else { final Element simpara = revHistoryDoc.createElement("simpara"); simpara.appendChild(revHistory); revHistoryDoc.getDocumentElement().appendChild(simpara); } } final TagWrapper author = buildData.getRequester() == null ? null : tagProvider.getTagByName(buildData.getRequester()); // Check if the app should be shutdown if (isShuttingDown.get()) { return; } // An assigned writer tag exists for the User so check if there is an AuthorInformation tuple for that writer if (author != null) { AuthorInformation authorInfo = EntityUtilities.getAuthorInformation(providerFactory, buildData.getServerEntities(), author.getId(), author.getRevision()); if (authorInfo != null) { final Element revision = generateRevision(buildData, revHistoryDoc, authorInfo); addRevisionToRevHistory(revHistory, revision); } else { // No AuthorInformation so Use the default value authorInfo = new AuthorInformation(-1, BuilderConstants.DEFAULT_AUTHOR_FIRSTNAME, BuilderConstants.DEFAULT_AUTHOR_LASTNAME, BuilderConstants.DEFAULT_EMAIL); final Element revision = generateRevision(buildData, revHistoryDoc, authorInfo); addRevisionToRevHistory(revHistory, revision); } } // No assigned writer exists for the uploader so use default values else { final AuthorInformation authorInfo = new AuthorInformation(-1, BuilderConstants.DEFAULT_AUTHOR_FIRSTNAME, BuilderConstants.DEFAULT_AUTHOR_LASTNAME, BuilderConstants.DEFAULT_EMAIL); final Element revision = generateRevision(buildData, revHistoryDoc, authorInfo); addRevisionToRevHistory(revHistory, revision); } // Add the revision history to the book final String fixedRevisionHistoryXml = DocBookBuildUtilities.convertDocumentToDocBookFormattedString( buildData.getDocBookVersion(), revHistoryDoc, "appendix", buildData.getEntityFileName(), getXMLFormatProperties()); addToZip(buildData.getBookLocaleFolder() + REVISION_HISTORY_FILE_NAME, fixedRevisionHistoryXml, buildData); } /** * Adds a revision element to the list of revisions in a revhistory element. This method ensures that the new revision is at * the top of the revhistory list. * * @param revHistory The revhistory element to add the revision to. * @param revision The revision element to be added into the revisionhistory element. */ private void addRevisionToRevHistory(final Node revHistory, final Node revision) { if (revHistory.hasChildNodes()) { revHistory.insertBefore(revision, revHistory.getFirstChild()); } else { revHistory.appendChild(revision); } } /** * Fills in the information required inside of a revision tag, for the Revision_History.xml file. * * @param buildData Information and data structures for the build. * @param xmlDoc An XML DOM document that contains key regex expressions. * @param authorInfo An AuthorInformation entity object containing the details for who requested the build. * @return Returns an XML element that represents a {@code <revision>} element initialised with the authors information. * @throws BuildProcessingException Thrown if an unexpected error occurs during building. */ protected Element generateRevision(final BuildData buildData, final Document xmlDoc, final AuthorInformation authorInfo) throws BuildProcessingException { if (authorInfo == null) { return null; } // Build up the revision final Element revision = xmlDoc.createElement("revision"); final Element revnumberEle = xmlDoc.createElement("revnumber"); revision.appendChild(revnumberEle); final Element revDateEle = xmlDoc.createElement("date"); final DateFormat dateFormatter = new SimpleDateFormat(BuilderConstants.REV_DATE_STRING_FORMAT, Locale.ENGLISH); revDateEle.setTextContent(dateFormatter.format(buildData.getBuildDate())); revision.appendChild(revDateEle); /* * Determine the revnumber to use. If we have an override specified then use that directly. If not then build up the * revision number using the Book Edition and Publication Number. The format to build it in is: <EDITION>-<PUBSNUMBER>. * If Edition only specifies a x or x.y version (eg 5 or 5.1) then postfix the version so it matches the x.y.z format * (eg 5.0.0). */ final String overrideRevnumber = buildData.getBuildOptions().getOverrides() .get(CSConstants.REVNUMBER_OVERRIDE); final String revnumber; if (overrideRevnumber == null) { revnumber = DocBookBuildUtilities.generateRevisionNumber(buildData.getContentSpec()); } else { revnumber = overrideRevnumber; } // Set the revision number in Revision_History.xml revnumberEle.setTextContent(revnumber); // Create the Author node final Element author = xmlDoc.createElement("author"); revision.appendChild(author); final Element firstName = xmlDoc.createElement("firstname"); firstName.setTextContent(authorInfo.getFirstName()); final Element lastName = xmlDoc.createElement("surname"); lastName.setTextContent(authorInfo.getLastName()); // Docbook 5 needs <firstname>/<surname> wrapped in <personname> if (buildData.getDocBookVersion() == DocBookVersion.DOCBOOK_50) { final Element personname = xmlDoc.createElement("personname"); author.appendChild(personname); personname.appendChild(firstName); personname.appendChild(lastName); } else { author.appendChild(firstName); author.appendChild(lastName); } final Element email = xmlDoc.createElement("email"); email.setTextContent( authorInfo.getEmail() == null ? BuilderConstants.DEFAULT_EMAIL : authorInfo.getEmail()); author.appendChild(email); // Create the Revision Messages final Element revDescription = xmlDoc.createElement("revdescription"); revision.appendChild(revDescription); final Element simplelist = xmlDoc.createElement("simplelist"); revDescription.appendChild(simplelist); // Add the custom revision messages if one or more exists. if (buildData.getBuildOptions().getRevisionMessages() != null && !buildData.getBuildOptions().getRevisionMessages().isEmpty()) { for (final String revMessage : buildData.getBuildOptions().getRevisionMessages()) { final Element revMemberEle = xmlDoc.createElement("member"); revMemberEle.setTextContent(revMessage); simplelist.appendChild(revMemberEle); } } // Add the revision information final Element listMemberEle = xmlDoc.createElement("member"); final ContentSpec contentSpec = buildData.getContentSpec(); if (contentSpec.getId() != null && contentSpec.getId() > 0) { if (contentSpec.getRevision() == null) { listMemberEle.setTextContent(String.format(BuilderConstants.BUILT_MSG, contentSpec.getId(), contentSpecProvider.getContentSpec(contentSpec.getId()).getRevision()) + (authorInfo.getAuthorId() > 0 ? (" by " + buildData.getRequester()) : "")); } else { listMemberEle.setTextContent( String.format(BuilderConstants.BUILT_MSG, contentSpec.getId(), contentSpec.getRevision()) + (authorInfo.getAuthorId() > 0 ? (" by " + buildData.getRequester()) : "")); } } else { listMemberEle.setTextContent(BuilderConstants.BUILT_FILE_MSG + (authorInfo.getAuthorId() > 0 ? (" by " + buildData.getRequester()) : "")); } simplelist.appendChild(listMemberEle); return revision; } /** * Builds a Chapter with a single paragraph, that contains a link to translate the Content Specification. * * @param buildData Information and data structures for the build.@return The Chapter represented as Docbook markup. */ private String buildTranslateCSChapter(final BuildData buildData) { final ContentSpec contentSpec = buildData.getContentSpec(); final TranslatedContentSpecWrapper translatedContentSpec = EntityUtilities .getClosestTranslatedContentSpecById(providerFactory, contentSpec.getId(), contentSpec.getRevision()); final String para; if (translatedContentSpec != null) { final String url = translatedContentSpec.getEditorURL(buildData.getZanataDetails(), buildData.getBuildLocale()); if (url != null) { para = DocBookUtilities.wrapInPara(DocBookUtilities.buildULink(url, "Translate this Content Spec")); } else { para = DocBookUtilities.wrapInPara( "No editor link available as this Content Specification hasn't been pushed for Translation."); } } else { para = DocBookUtilities.wrapInPara( "No editor link available as this Content Specification hasn't been pushed for Translation."); } if (contentSpec.getBookType() == BookType.ARTICLE || contentSpec.getBookType() == BookType.ARTICLE_DRAFT) { return DocBookUtilities.buildSection(para, "Content Specification"); } else { return DocBookUtilities.buildChapter(para, "Content Specification"); } } /** * Builds the Error Chapter that contains all warnings and errors. It also builds a glossary to define most of the error * messages. * * @param buildData Information and data structures for the build. * @return A docbook formatted string representation of the error chapter. */ private String buildErrorChapter(final BuildData buildData) { log.info("\tBuilding Error Chapter"); String errorItemizedLists = ""; if (buildData.getErrorDatabase().hasItems(buildData.getBuildLocale())) { for (final TopicErrorData topicErrorData : buildData.getErrorDatabase() .getErrors(buildData.getBuildLocale())) { // Check if the app should be shutdown if (isShuttingDown.get()) { return null; } final BaseTopicWrapper<?> topic = topicErrorData.getTopic(); final List<String> topicErrorItems = new ArrayList<String>(); final String tags = EntityUtilities.getCommaSeparatedTagList(topic); final String url = topic.getPressGangURL(); topicErrorItems.add( DocBookUtilities.buildListItem("INFO: " + StringEscapeUtils.escapeXml(topic.getTitle()))); if (tags != null && !tags.isEmpty()) { topicErrorItems .add(DocBookUtilities.buildListItem("INFO: " + StringEscapeUtils.escapeXml(tags))); } if (buildData.getDocBookVersion() == DocBookVersion.DOCBOOK_50) { topicErrorItems.add(DocBookUtilities .buildListItem("INFO: <link xlink:href=\"" + url + "\">Topic URL</link>")); } else { topicErrorItems.add( DocBookUtilities.buildListItem("INFO: <ulink url=\"" + url + "\">Topic URL</ulink>")); } for (final String error : topicErrorData.getItemsOfType(ErrorLevel.ERROR)) { topicErrorItems.add(DocBookUtilities.buildListItem("ERROR: " + error)); } for (final String warning : topicErrorData.getItemsOfType(ErrorLevel.WARNING)) { topicErrorItems.add(DocBookUtilities.buildListItem("WARNING: " + warning)); } /* * this should never be false, because a topic will only be listed in the errors collection once a error or * warning has been added. The count of 2 comes from the standard list items we added above for the title and * url. */ if (topicErrorItems.size() > 2) { final String title; if (topic instanceof TranslatedTopicWrapper) { title = "Topic ID " + topic.getTopicId() + ", Revision " + topic.getTopicRevision(); } else { title = "Topic ID " + topic.getTopicId(); } final String id = topic.getErrorXRefId(); errorItemizedLists += DocBookUtilities.wrapListItems(buildData.getDocBookVersion(), topicErrorItems, title, id); } } // Create the glossary final String errorGlossary = buildErrorChapterGlossary(buildData, "Compiler Glossary"); if (errorGlossary != null) { errorItemizedLists += errorGlossary; } } else { errorItemizedLists = "<para>No Errors Found</para>"; } if (buildData.getContentSpec().getBookType() == BookType.ARTICLE || buildData.getContentSpec().getBookType() == BookType.ARTICLE_DRAFT) { return DocBookBuildUtilities.addDocBookPreamble(buildData.getDocBookVersion(), DocBookUtilities.buildSection(errorItemizedLists, "Compiler Output"), "section", buildData.getEntityFileName()); } else { return DocBookBuildUtilities.addDocBookPreamble(buildData.getDocBookVersion(), DocBookUtilities.buildChapter(errorItemizedLists, "Compiler Output"), "chapter", buildData.getEntityFileName()); } } /** * Builds the Glossary used in the Error Chapter. * * @param buildData Information and data structures for the build. * @param title The title for the glossary. * @return A docbook formatted string representation of the glossary. */ private String buildErrorChapterGlossary(final BuildData buildData, final String title) { final StringBuilder glossary = new StringBuilder("<glossary>"); // Add the title of the glossary glossary.append("<title>"); if (title != null) { glossary.append(title); } glossary.append("</title>"); // Add generic error messages // No Content Warning glossary.append(DocBookUtilities.wrapInGlossEntry( DocBookUtilities.wrapInGlossTerm("\"" + BuilderConstants.WARNING_EMPTY_TOPIC_XML + "\""), DocBookUtilities.wrapInItemizedGlossDef(null, BuilderConstants.WARNING_NO_CONTENT_TOPIC_DEFINITION))); // Invalid XML entity or element glossary.append(DocBookUtilities.wrapInGlossEntry( DocBookUtilities.wrapInGlossTerm("\"" + BuilderConstants.ERROR_INVALID_XML_CONTENT + "\""), DocBookUtilities.wrapInItemizedGlossDef(null, BuilderConstants.ERROR_INVALID_XML_CONTENT_DEFINITION))); // No Content Error glossary.append(DocBookUtilities.wrapInGlossEntry( DocBookUtilities.wrapInGlossTerm("\"" + BuilderConstants.ERROR_BAD_XML_STRUCTURE + "\""), DocBookUtilities.wrapInItemizedGlossDef(null, BuilderConstants.ERROR_BAD_XML_STRUCTURE_DEFINITION))); // Invalid Docbook XML Error glossary.append(DocBookUtilities.wrapInGlossEntry( DocBookUtilities.wrapInGlossTerm("\"" + BuilderConstants.ERROR_INVALID_TOPIC_XML + "\""), DocBookUtilities.wrapInItemizedGlossDef(null, BuilderConstants.ERROR_INVALID_TOPIC_XML_DEFINITION))); // Invalid Injections Error glossary.append(DocBookUtilities.wrapInGlossEntry( DocBookUtilities.wrapInGlossTerm("\"" + BuilderConstants.ERROR_INVALID_INJECTIONS + "\""), DocBookUtilities.wrapInItemizedGlossDef(null, BuilderConstants.ERROR_INVALID_INJECTIONS_DEFINITION))); // Possible Invalid Injections Warning glossary.append(DocBookUtilities.wrapInGlossEntry( DocBookUtilities .wrapInGlossTerm("\"... " + BuilderConstants.WARNING_POSSIBLE_INVALID_INJECTIONS + "\""), DocBookUtilities.wrapInItemizedGlossDef(null, BuilderConstants.WARNING_POSSIBLE_INVALID_INJECTIONS_DEFINITION))); // Add the glossary terms and definitions if (buildData.isTranslationBuild()) { // Incomplete translation warning glossary.append(DocBookUtilities.wrapInGlossEntry( DocBookUtilities.wrapInGlossTerm("\"" + BuilderConstants.WARNING_INCOMPLETE_TRANSLATION + "\""), DocBookUtilities.wrapInItemizedGlossDef(null, BuilderConstants.WARNING_INCOMPLETE_TRANSLATED_TOPIC_DEFINITION))); // Fuzzy translation warning glossary.append(DocBookUtilities.wrapInGlossEntry( DocBookUtilities.wrapInGlossTerm("\"" + BuilderConstants.WARNING_FUZZY_TRANSLATION + "\""), DocBookUtilities.wrapInItemizedGlossDef(null, BuilderConstants.WARNING_FUZZY_TRANSLATED_TOPIC_DEFINITION))); // Untranslated Content warning glossary.append(DocBookUtilities.wrapInGlossEntry( DocBookUtilities.wrapInGlossTerm("\"" + BuilderConstants.WARNING_UNTRANSLATED_TOPIC + "\""), DocBookUtilities.wrapInItemizedGlossDef(null, BuilderConstants.WARNING_UNTRANSLATED_TOPIC_DEFINITION))); // Non Pushed Translation Content warning glossary.append(DocBookUtilities.wrapInGlossEntry( DocBookUtilities.wrapInGlossTerm("\"" + BuilderConstants.WARNING_NONPUSHED_TOPIC + "\""), DocBookUtilities.wrapInItemizedGlossDef(null, BuilderConstants.WARNING_NONPUSHED_TOPIC_DEFINITION))); // Old Translation Content warning glossary.append(DocBookUtilities.wrapInGlossEntry( DocBookUtilities.wrapInGlossTerm("\"" + BuilderConstants.WARNING_OLD_TRANSLATED_TOPIC + "\""), DocBookUtilities.wrapInItemizedGlossDef(null, BuilderConstants.WARNING_OLD_TRANSLATED_TOPIC_DEFINITION))); // Old Untranslated Content warning glossary.append(DocBookUtilities.wrapInGlossEntry( DocBookUtilities.wrapInGlossTerm("\"" + BuilderConstants.WARNING_OLD_UNTRANSLATED_TOPIC + "\""), DocBookUtilities.wrapInItemizedGlossDef(null, BuilderConstants.WARNING_OLD_UNTRANSLATED_TOPIC_DEFINITION))); } glossary.append("</glossary>"); return glossary.toString(); } /** * Builds a Report Chapter to be included in the book that displays a count of different types of errors and then a table to * list the errors, providing links and basic topic data. * * @param buildData Information and data structures for the build. * @return The Docbook Report Chapter formatted as a String. */ private String buildReportChapter(final BuildData buildData) { log.info("\tBuilding Report Chapter"); final ContentSpec contentSpec = buildData.getContentSpec(); final String locale = buildData.getBuildLocale(); final ZanataDetails zanataDetails = buildData.getZanataDetails(); String reportChapter = ""; final List<TopicErrorData> noContentTopics = buildData.getErrorDatabase().getErrorsOfType(locale, ErrorType.NO_CONTENT); final List<TopicErrorData> invalidInjectionTopics = buildData.getErrorDatabase().getErrorsOfType(locale, ErrorType.INVALID_INJECTION); final List<TopicErrorData> invalidContentTopics = buildData.getErrorDatabase().getErrorsOfType(locale, ErrorType.INVALID_CONTENT); final List<TopicErrorData> invalidImageTopics = buildData.getErrorDatabase().getErrorsOfType(locale, ErrorType.INVALID_IMAGES); final List<TopicErrorData> untranslatedTopics = buildData.getErrorDatabase().getErrorsOfType(locale, ErrorType.UNTRANSLATED); final List<TopicErrorData> incompleteTranslatedTopics = buildData.getErrorDatabase().getErrorsOfType(locale, ErrorType.INCOMPLETE_TRANSLATION); final List<TopicErrorData> fuzzyTranslatedTopics = buildData.getErrorDatabase().getErrorsOfType(locale, ErrorType.FUZZY_TRANSLATION); final List<TopicErrorData> notPushedTranslatedTopics = buildData.getErrorDatabase().getErrorsOfType(locale, ErrorType.NOT_PUSHED_FOR_TRANSLATION); final List<TopicErrorData> oldTranslatedTopics = buildData.getErrorDatabase().getErrorsOfType(locale, ErrorType.OLD_TRANSLATION); final List<TopicErrorData> oldUntranslatedTopics = buildData.getErrorDatabase().getErrorsOfType(locale, ErrorType.OLD_UNTRANSLATED); final List<String> list = new LinkedList<String>(); list.add(DocBookUtilities.buildListItem("Total Number of Errors: " + getNumErrors())); list.add(DocBookUtilities.buildListItem("Total Number of Warnings: " + getNumWarnings())); list.add(DocBookUtilities.buildListItem("Number of Topics with No Content: " + noContentTopics.size())); list.add(DocBookUtilities .buildListItem("Number of Topics with Invalid Injection points: " + invalidInjectionTopics.size())); list.add(DocBookUtilities .buildListItem("Number of Topics with Invalid Content: " + invalidContentTopics.size())); list.add(DocBookUtilities .buildListItem("Number of Topics with Invalid Image references: " + invalidImageTopics.size())); if (buildData.isTranslationBuild()) { list.add(DocBookUtilities.buildListItem("Number of Topics that haven't been pushed for Translation: " + notPushedTranslatedTopics.size())); list.add(DocBookUtilities .buildListItem("Number of Topics that haven't been Translated: " + untranslatedTopics.size())); list.add(DocBookUtilities.buildListItem( "Number of Topics that have incomplete Translations: " + incompleteTranslatedTopics.size())); list.add(DocBookUtilities.buildListItem( "Number of Topics that have fuzzy Translations: " + fuzzyTranslatedTopics.size())); list.add(DocBookUtilities.buildListItem( "Number of Topics that haven't been Translated but are using previous revisions: " + oldUntranslatedTopics.size())); list.add(DocBookUtilities .buildListItem("Number of Topics that have been Translated using a previous revision: " + oldTranslatedTopics.size())); } reportChapter += DocBookUtilities.wrapListItems(list, "Build Statistics"); // Add a link to show the zanata statistics if (buildData.isTranslationBuild()) { reportChapter += generateAllTopicZanataUrl(buildData); } final boolean showEditorLinks = buildData.getBuildOptions().getInsertEditorLinks(); // Create the Report Tables reportChapter += ReportUtilities.buildReportTable(noContentTopics, "Topics that have no Content", showEditorLinks, zanataDetails); reportChapter += ReportUtilities.buildReportTable(invalidContentTopics, "Topics that have Invalid XML Content", showEditorLinks, zanataDetails); reportChapter += ReportUtilities.buildReportTable(invalidInjectionTopics, "Topics that have Invalid Injection points in the XML", showEditorLinks, zanataDetails); reportChapter += ReportUtilities.buildReportTable(invalidImageTopics, "Topics that have Invalid Image references in the XML", showEditorLinks, zanataDetails); if (buildData.isTranslationBuild()) { reportChapter += ReportUtilities.buildReportTable(notPushedTranslatedTopics, "Topics that haven't been pushed for Translation", showEditorLinks, zanataDetails); reportChapter += ReportUtilities.buildReportTable(untranslatedTopics, "Topics that haven't been Translated", showEditorLinks, zanataDetails); reportChapter += ReportUtilities.buildReportTable(incompleteTranslatedTopics, "Topics that have Incomplete Translations", showEditorLinks, zanataDetails); reportChapter += ReportUtilities.buildReportTable(fuzzyTranslatedTopics, "Topics that have fuzzy Translations", showEditorLinks, zanataDetails); reportChapter += ReportUtilities.buildReportTable(oldUntranslatedTopics, "Topics that haven't been Translated but are using previous revisions", showEditorLinks, zanataDetails); reportChapter += ReportUtilities.buildReportTable(oldTranslatedTopics, "Topics that have been Translated using a previous revision", showEditorLinks, zanataDetails); } if (contentSpec.getBookType() == BookType.ARTICLE || contentSpec.getBookType() == BookType.ARTICLE_DRAFT) { return DocBookBuildUtilities.addDocBookPreamble(buildData.getDocBookVersion(), DocBookUtilities.buildSection(reportChapter, "Status Report"), "section", buildData.getEntityFileName()); } else { return DocBookBuildUtilities.addDocBookPreamble(buildData.getDocBookVersion(), DocBookUtilities.buildChapter(reportChapter, "Status Report"), "chapter", buildData.getEntityFileName()); } } /** * Generates a set of docbook paragraphs containing links to all the Topics in Zanata. * * @param buildData Information and data structures for the build.@return The docbook generated content. */ protected String generateAllTopicZanataUrl(final BuildData buildData) { final ZanataDetails zanataDetails = buildData.getZanataDetails(); final String zanataServerUrl = zanataDetails == null ? null : zanataDetails.getServer(); final String zanataProject = zanataDetails == null ? null : zanataDetails.getProject(); final String zanataVersion = zanataDetails == null ? null : zanataDetails.getVersion(); String reportChapter = ""; if (zanataServerUrl != null && !zanataServerUrl.isEmpty() && zanataProject != null && !zanataProject.isEmpty() && zanataVersion != null && !zanataVersion.isEmpty()) { final List<StringBuilder> zanataUrls = new ArrayList<StringBuilder>(); StringBuilder zanataUrl = new StringBuilder(zanataServerUrl); zanataUrls.add(zanataUrl); zanataUrl.append("webtrans/Application.html?project=" + zanataProject); zanataUrl.append("&"); zanataUrl.append("iteration=" + zanataVersion); zanataUrl.append("&"); zanataUrl.append("localeId=" + buildData.getBuildLocale()); // Add all the Topic Zanata Ids final List<TranslatedTopicWrapper> topics = buildData.getBuildDatabase().getAllTopics(); int andCount = 0; for (final TranslatedTopicWrapper topic : topics) { // Check to make sure the topic has been pushed for translation if (!EntityUtilities.isDummyTopic(topic) || EntityUtilities.hasBeenPushedForTranslation(topic)) { zanataUrl.append("&"); andCount++; zanataUrl.append("doc=" + ((TranslatedTopicWrapper) topic).getZanataId()); } // If the URL gets too big create a second, third, etc... URL. if (zanataUrl.length() > (MAX_URL_LENGTH + andCount * 4)) { zanataUrl = new StringBuilder(zanataServerUrl); zanataUrls.add(zanataUrl); zanataUrl.append("webtrans/Application.html?project=" + zanataProject); zanataUrl.append("&"); zanataUrl.append("iteration=" + zanataVersion); zanataUrl.append("&"); zanataUrl.append("localeId=" + buildData.getBuildLocale()); } } // Add the CSP Zanata ID final TranslatedContentSpecWrapper translatedContentSpec = EntityUtilities .getClosestTranslatedContentSpecById(providerFactory, buildData.getContentSpec().getId(), buildData.getContentSpec().getRevision()); if (translatedContentSpec != null) { zanataUrl.append("&"); zanataUrl.append("doc=" + translatedContentSpec.getZanataId()); } // Generate the docbook elements for the links for (int i = 1; i <= zanataUrls.size(); i++) { final String para; if (zanataUrls.size() > 1) { para = DocBookUtilities.wrapInPara(DocBookUtilities.buildULink(zanataUrls.get(i - 1).toString(), "View Topics and Statistics in Zanata (" + i + "/" + zanataUrls.size() + ")")); } else { para = DocBookUtilities.wrapInPara(DocBookUtilities.buildULink(zanataUrl.toString(), "View Topics and Statistics in Zanata")); } reportChapter += para; } } return reportChapter; } /** * Processes the Topics in the BuildDatabase and builds up the images found within the topics XML. If the image reference is * blank or invalid it is replaced by the fail penguin image. * * @param buildData Information and data structures for the build. */ @SuppressWarnings("unchecked") private void processImageLocations(final BuildData buildData) { final List<Integer> topicIds = buildData.getBuildDatabase().getTopicIds(); for (final Integer topicId : topicIds) { final ITopicNode topicNode = buildData.getBuildDatabase().getTopicNodesForTopicID(topicId).get(0); final BaseTopicWrapper<?> topic = topicNode.getTopic(); if (log.isDebugEnabled()) log.debug("\tProcessing SpecTopic " + topicNode.getId() + (topicNode.getRevision() != null ? (", " + "Revision " + topicNode.getRevision()) : "")); /* * Images have to be in the image folder in Publican. Here we loop through all the imagedata elements and fix up any * reference to an image that is not in the images folder. */ final List<Node> images = XMLUtilities.getChildNodes(topicNode.getXMLDocument(), "imagedata", "inlinegraphic"); for (final Node imageNode : images) { final NamedNodeMap attributes = imageNode.getAttributes(); if (attributes != null) { final Node fileRefAttribute = attributes.getNamedItem("fileref"); if (fileRefAttribute != null && (fileRefAttribute.getNodeValue() == null || fileRefAttribute.getNodeValue().isEmpty())) { fileRefAttribute.setNodeValue("images/" + BuilderConstants.FAILPENGUIN_PNG_NAME + ".jpg"); buildData.getImageLocations() .add(new TopicImageData(topic, fileRefAttribute.getNodeValue())); } else if (fileRefAttribute != null && fileRefAttribute.getNodeValue() != null) { final String fileRefValue = fileRefAttribute.getNodeValue(); if (BuilderConstants.IMAGE_FILE_REF_PATTERN.matcher(fileRefValue).matches()) { if (fileRefValue.startsWith("./images/")) { fileRefAttribute.setNodeValue(fileRefValue.substring(2)); } else if (!fileRefValue.startsWith("images/")) { fileRefAttribute.setNodeValue("images/" + fileRefValue); } buildData.getImageLocations() .add(new TopicImageData(topic, fileRefAttribute.getNodeValue())); } else if (!BuilderConstants.COMMON_CONTENT_FILE_REF_PATTERN.matcher(fileRefValue) .matches()) { // The file isn't common content or a pressgang image so mark it as a missing image. fileRefAttribute .setNodeValue("images/" + BuilderConstants.FAILPENGUIN_PNG_NAME + ".jpg"); buildData.getImageLocations() .add(new TopicImageData(topic, fileRefAttribute.getNodeValue())); } } } } } } /** * Validates the XML after the first set of injections have been processed. * * @param buildData Information and data structures for the build. * @param topicNode The topic that is being validated. * @param topicDoc A Document object that holds the Topic's XML * @return The validate document or a template if it failed validation. * @throws BuildProcessingException Thrown if an unexpected error occurs during building. */ @SuppressWarnings("unchecked") private boolean validateTopicXML(final BuildData buildData, final ITopicNode topicNode, final Document topicDoc) throws BuildProcessingException { final XMLValidator validator = new XMLValidator(); final ContentSpec contentSpec = buildData.getContentSpec(); final BaseTopicWrapper<?> topic = topicNode.getTopic(); final StringBuilder entity = new StringBuilder(CSConstants.DUMMY_CS_NAME_ENT_FILE); // Add any custom entities if (!isNullOrEmpty(contentSpec.getEntities())) { entity.append(contentSpec.getEntities()); } // Wrap the document so it can be validated. final String xml = XMLUtilities.convertDocumentToString(topicDoc, ENCODING); final Pair<String, String> wrappedTopic = DocBookUtilities.wrapForValidation(buildData.getDocBookVersion(), xml); final String fixedTopicXml = wrappedTopic.getSecond(); final String rootElementName = wrappedTopic.getFirst(); // Get the schema/dtd as well as any additional content required for validation final String titleXML; final String docbookFileName; final XMLValidator.ValidationMethod validationMethod; final byte[] docbookSchema; if (buildData.getDocBookVersion() == DocBookVersion.DOCBOOK_50) { docbookFileName = BuilderConstants.DOCBOOK_50_RNG; validationMethod = XMLValidator.ValidationMethod.RELAXNG; docbookSchema = docbookRng.getValue(); titleXML = DocBookUtilities.addDocBook50Namespace( "<section><title>" + topic.getTitle() + "</title><para /></section>", "section"); entity.append(docbook45Entities); } else { docbookFileName = BuilderConstants.ROCBOOK_45_DTD; validationMethod = XMLValidator.ValidationMethod.DTD; docbookSchema = rocbookDtd.getValue(); titleXML = "<section><title>" + topic.getTitle() + "</title><para /></section>"; } final String entityData = entity.toString(); // First check to see if the title is valid XML if (!validator.validate(validationMethod, titleXML, docbookFileName, docbookSchema, entityData, "section")) { // The title is invalid so replace it with something that is valid topic.setTitle("Invalid Topic"); DocBookUtilities.setSectionTitle(buildData.getDocBookVersion(), topic.getTitle(), topicDoc); } // Validate the topic against its DTD/Schema if (!validator.validate(validationMethod, fixedTopicXml, docbookFileName, docbookSchema, entityData, rootElementName)) { // Store the error message final String errorMsg = validator.getErrorText(); final String cleanedErrorMsg = errorMsg.replace("|", " | ").replace(",", ", ").replaceFirst("\\.$", ""); final String xmlStringInCDATA = DocBookBuildUtilities.convertDocumentToCDATAFormattedString(topicDoc, getXMLFormatProperties()); buildData.getErrorDatabase().addError(topic, ErrorType.INVALID_CONTENT, BuilderConstants.ERROR_INVALID_TOPIC_XML + " The error is <emphasis>" + StringEscapeUtils.escapeXml(cleanedErrorMsg) + "</emphasis>. The processed XML is <programlisting>" + xmlStringInCDATA + "</programlisting>"); DocBookBuildUtilities.setTopicNodeXMLForError(buildData, topicNode, getErrorInvalidValidationTopicTemplate().getValue()); return false; } // Check the content of the XML for things not picked up by DTD validation final List<String> xmlErrors = DocBookBuildUtilities.checkTopicForInvalidContent(topicNode, topic, topicDoc, buildData); if (xmlErrors.size() > 0) { final String xmlStringInCDATA = DocBookBuildUtilities.convertDocumentToCDATAFormattedString(topicDoc, getXMLFormatProperties()); // Escape the XML Errors as they will currently be in plain text final List<String> escapedXmlErrors = new ArrayList<String>(); for (final String xmlError : xmlErrors) { escapedXmlErrors.add(StringEscapeUtils.escapeXml(xmlError)); } // Add the error and processed XML to the error message final String errorMessage; if (escapedXmlErrors.size() > 1) { errorMessage = "<itemizedlist><listitem><para>" + CollectionUtilities .toSeperatedString(escapedXmlErrors, "</para></listitem><listitem><para>") + "</para></listitem></itemizedlist>"; } else { errorMessage = escapedXmlErrors.get(0); } buildData.getErrorDatabase().addError(topic, ErrorType.INVALID_CONTENT, BuilderConstants.ERROR_INVALID_TOPIC_XML + " " + errorMessage + "</para><para>The processed XML is <programlisting>" + xmlStringInCDATA + "</programlisting>"); DocBookBuildUtilities.setTopicNodeXMLForError(buildData, topicNode, getErrorInvalidValidationTopicTemplate().getValue()); return false; } return true; } /** * Process a topic and add the section info information. This information consists of the keywordset information. The * keywords are populated using the tags assigned to the topic. * * @param buildData Information and data structures for the build. * @param topic The Topic to create the sectioninfo for. * @param doc The XML Document DOM object for the topics XML. */ protected void processTopicSectionInfo(final BuildData buildData, final BaseTopicWrapper<?> topic, final Document doc) { if (doc == null || topic == null) return; final String infoName; if (buildData.getDocBookVersion() == DocBookVersion.DOCBOOK_50) { infoName = "info"; } else { infoName = DocBookUtilities.TOPIC_ROOT_SECTIONINFO_NODE_NAME; } final CollectionWrapper<TagWrapper> tags = topic.getTags(); final List<Integer> seoCategoryIds = buildData.getServerSettings().getSEOCategoryIds(); if (seoCategoryIds != null && !seoCategoryIds.isEmpty() && tags != null && tags.getItems() != null && tags.getItems().size() > 0) { // Find the sectioninfo node in the document, or create one if it doesn't exist final Element sectionInfo; final List<Node> sectionInfoNodes = XMLUtilities.getDirectChildNodes(doc.getDocumentElement(), infoName); if (sectionInfoNodes.size() == 1) { sectionInfo = (Element) sectionInfoNodes.get(0); } else { sectionInfo = doc.createElement(infoName); } // Build up the keywordset final Element keywordSet = doc.createElement("keywordset"); final List<TagWrapper> tagItems = tags.getItems(); for (final TagWrapper tag : tagItems) { if (tag.getName() == null || tag.getName().isEmpty()) continue; if (tag.containedInCategories(seoCategoryIds)) { final Element keyword = doc.createElement("keyword"); keyword.setTextContent(tag.getName()); keywordSet.appendChild(keyword); } } // Only update the section info if we've added data if (keywordSet.hasChildNodes()) { sectionInfo.appendChild(keywordSet); DocBookUtilities.setInfo(buildData.getDocBookVersion(), sectionInfo, doc.getDocumentElement()); } } } /** * This method does a pass over all the spec nodes and attempts to create unique Fixed URL if one does not * already exist. * * @param buildData Information and data structures for the build. * @return True if the fixed url property tags were able to be created, and false otherwise. */ protected boolean doFixedURLsPass(final BuildData buildData) throws BuildProcessingException { log.info("Doing Fixed URL Pass"); final Set<String> processedFileNames = new HashSet<String>(); final Set<SpecNode> nodesWithoutFixedUrls = new HashSet<SpecNode>(); boolean success = true; try { // Collect any current fixed urls or nodes that need configuring FixedURLGenerator.collectFixedUrlInformation(buildData.getBuildDatabase().getAllSpecNodes(), nodesWithoutFixedUrls, processedFileNames); // Warn the user about using temporary fixed urls if (!nodesWithoutFixedUrls.isEmpty()) { log.info("\tUsing " + nodesWithoutFixedUrls.size() + " temporary Fixed URLs"); } // Generate the fixed urls for the missing nodes FixedURLGenerator.generateFixedUrlForNodes(nodesWithoutFixedUrls, processedFileNames, buildData.getServerEntities().getFixedUrlPropertyTagId()); } catch (Exception e) { success = false; log.debug("", e); throw new BuildProcessingException( "Failed to update the Fixed URLs. Please try again and if the issue persists please log a bug."); } return success; } /** * Adds a file to the files ZIP. * * @param path The path to add the file to. * @param file The file to add to the ZIP. * @param buildData Information and data structures for the build. */ protected void addToZip(final String path, final String file, BuildData buildData) throws BuildProcessingException { try { buildData.getOutputFiles().put(path, file.getBytes(ENCODING)); } catch (UnsupportedEncodingException e) { /* UTF-8 is a valid format so this should exception should never get thrown */ throw new BuildProcessingException(e); } } /** * Adds a file to the files ZIP. * * @param path The path to add the file to. * @param data The data to add to the ZIP. * @param buildData */ protected void addToZip(final String path, final byte[] data, final BuildData buildData) { buildData.getOutputFiles().put(path, data); } /** * Adds the additional files defined in a content spec to the book. * * @param buildData Information and data structures for the build. */ protected void addAdditionalFilesToBook(final BuildData buildData) throws BuildProcessingException { final FileProvider fileProvider = providerFactory.getProvider(FileProvider.class); final ContentSpec contentSpec = buildData.getContentSpec(); if (contentSpec.getFiles() != null) { log.info("\tDownloading Additional Files"); for (final org.jboss.pressgang.ccms.contentspec.File file : contentSpec.getFiles()) { try { final FileWrapper fileEntity = fileProvider.getFile(file.getId(), file.getRevision()); // Find the file that matches this locale. If the locale isn't found then use the default locale LanguageFileWrapper languageFileFile = null; if (fileEntity.getLanguageFiles() != null && fileEntity.getLanguageFiles().getItems() != null) { final List<LanguageFileWrapper> languageFiles = fileEntity.getLanguageFiles().getItems(); for (final LanguageFileWrapper languageFile : languageFiles) { if (languageFile.getLocale().getValue().equals(buildData.getBuildLocale())) { languageFileFile = languageFile; } else if (languageFile.getLocale().getValue().equals(buildData.getDefaultLocale()) && languageFileFile == null) { languageFileFile = languageFile; } } } if (languageFileFile != null && languageFileFile.getFileData() != null) { // Determine the file path final String filePath; if (!isNullOrEmpty(fileEntity.getFilePath())) { if (fileEntity.getFilePath().endsWith("/") || fileEntity.getFilePath().endsWith("\\")) { filePath = fileEntity.getFilePath(); } else { filePath = fileEntity.getFilePath() + "/"; } } else { filePath = ""; } // Explode the ZIP archive if requested if (fileEntity.isExplodeArchive()) { try { final Map<String, byte[]> files = ZipUtilities .unzipFile(languageFileFile.getFileData(), false); for (final Entry<String, byte[]> entry : files.entrySet()) { addToZip(buildData.getBookFilesFolder() + filePath + entry.getKey(), entry.getValue(), buildData); } } catch (IOException e) { throw new BuildProcessingException(e); } } else { addToZip(buildData.getBookFilesFolder() + filePath + fileEntity.getFilename(), languageFileFile.getFileData(), buildData); } } else { throw new BuildProcessingException( "File ID " + fileEntity.getId() + " has no language files!"); } } catch (NotFoundException e) { throw new BuildProcessingException("File ID " + file.getId() + " could not be found!"); } } } } }