/* 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.server.utils; import static com.google.common.base.Strings.isNullOrEmpty; import javax.persistence.EntityManager; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.regex.Matcher; import java.util.regex.Pattern; import org.jboss.pressgang.ccms.model.Topic; import org.jboss.pressgang.ccms.utils.common.XMLUtilities; import org.jboss.pressgang.ccms.utils.constants.CommonConstants; import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.Node; /** * For now this has been copied from the UI project. Responsibility for resolving injections will probably be * moved from the client to the server. */ public class InjectionResolver { /** * This token is replaced in a URL with the target topic id when resolving injections */ public static final String HOST_URL_ID_TOKEN = "#TOPICID#"; /** * Used to identify that an <orderedlist> should be generated for the injection point */ protected static final int ORDEREDLIST_INJECTION_POINT = 1; /** * Used to identify that an <itemizedlist> should be generated for the injection point */ protected static final int ITEMIZEDLIST_INJECTION_POINT = 2; /** * Used to identify that an <xref> should be generated for the injection point */ protected static final int XREF_INJECTION_POINT = 3; /** * Used to identify that an <xref> should be generated for the injection point */ protected static final int LIST_INJECTION_POINT = 4; /** * This text identifies an option task in a list */ protected static final String OPTIONAL_MARKER = "OPT:"; /** * The text to be prefixed to a list item if a topic is optional */ protected static final String OPTIONAL_LIST_PREFIX = "Optional: "; protected static final String ID_RE = "(\\d+|T(\\d+|(\\-[ ]*[A-Za-z][A-Za-z\\d\\-_]*)))"; /** * A regular expression that identifies a topic id */ protected static final String OPTIONAL_TOPIC_ID_RE = "(" + OPTIONAL_MARKER + "\\s*)?" + ID_RE; /** * A regular expression that matches an InjectSequence custom injection point */ private static final String CUSTOM_INJECTION_SEQUENCE = "\\s*InjectSequence:\\s*((\\s*" + OPTIONAL_TOPIC_ID_RE + "\\s*," + ")*(\\s*" + OPTIONAL_TOPIC_ID_RE + ",?))\\s*"; public static final Pattern CUSTOM_INJECTION_SEQUENCE_RE = Pattern.compile("<!--" + CUSTOM_INJECTION_SEQUENCE + "-->", Pattern.MULTILINE); public static final Pattern DOC_CUSTOM_INJECTION_SEQUENCE_RE = Pattern.compile("^" + CUSTOM_INJECTION_SEQUENCE + "$"); /** * A regular expression that matches an InjectList custom injection point */ private static final String CUSTOM_INJECTION_LIST = "\\s*InjectList:\\s*((\\s*" + OPTIONAL_TOPIC_ID_RE + "\\s*," + ")*(\\s*" + OPTIONAL_TOPIC_ID_RE + ",?))\\s*"; public static final Pattern CUSTOM_INJECTION_LIST_RE = Pattern.compile("<!--" + CUSTOM_INJECTION_LIST + "-->", Pattern.MULTILINE); public static final Pattern DOC_CUSTOM_INJECTION_LIST_RE = Pattern.compile("^" + CUSTOM_INJECTION_LIST + "$"); private static final String CUSTOM_INJECTION_LISTITEMS = "\\s*InjectListItems:\\s*((\\s*" + OPTIONAL_TOPIC_ID_RE + "\\s*," + ")*(\\s*" + OPTIONAL_TOPIC_ID_RE + ",?))\\s*"; public static final Pattern CUSTOM_INJECTION_LISTITEMS_RE = Pattern.compile("<!--" + CUSTOM_INJECTION_LISTITEMS + "-->", Pattern.MULTILINE); public static final Pattern DOC_CUSTOM_INJECTION_LISTITEMS_RE = Pattern.compile("^" + CUSTOM_INJECTION_LISTITEMS + "$"); private static final String CUSTOM_ALPHA_SORT_INJECTION_LIST = "\\s*InjectListAlphaSort:\\s*((\\s*" + OPTIONAL_TOPIC_ID_RE + "\\s*," + ")*(\\s*" + OPTIONAL_TOPIC_ID_RE + ",?))\\s*"; public static final Pattern CUSTOM_ALPHA_SORT_INJECTION_LIST_RE = Pattern.compile("<!--" + CUSTOM_ALPHA_SORT_INJECTION_LIST + "-->", Pattern.MULTILINE); public static final Pattern DOC_CUSTOM_ALPHA_SORT_INJECTION_LIST_RE = Pattern.compile("^" + CUSTOM_ALPHA_SORT_INJECTION_LIST + "$"); /** * A regular expression that matches an Inject custom injection point */ private static final String CUSTOM_INJECTION_SINGLE = "\\s*Inject:\\s*(" + OPTIONAL_TOPIC_ID_RE + ")\\s*"; public static final Pattern CUSTOM_INJECTION_SINGLE_RE = Pattern.compile("<!--" + CUSTOM_INJECTION_SINGLE + "-->", Pattern.MULTILINE); public static final Pattern DOC_CUSTOM_INJECTION_SINGLE_RE = Pattern.compile("^" + CUSTOM_INJECTION_SINGLE + "$"); /** * Process a XML Document to resolve any custom injection references so that they can be rendered/validated. This method will * transform the injections into links to the editor for each injected topic. It will not make any external calls, * so the text displayed will be "Topic <ID>". This also means that any injections that use sorting will not be sorted, * since we don't have access to the topic titles. * * @return The processed XML with the injections resolved. */ public static String resolveInjections(final EntityManager entityManager, final Integer xmlFormat, final String xml, final String hostUrl) { // Make sure we have something to process. if (isNullOrEmpty(xml)) { return xml; } // Process the comments to get the injection references final Map<Integer, Map<String, List<InjectionData>>> injections = new HashMap<Integer, Map<String, List<InjectionData>>>(); processInjections(xml, injections, ORDEREDLIST_INJECTION_POINT, CUSTOM_INJECTION_SEQUENCE_RE); processInjections(xml, injections, XREF_INJECTION_POINT, CUSTOM_INJECTION_SINGLE_RE); processInjections(xml, injections, ITEMIZEDLIST_INJECTION_POINT, CUSTOM_INJECTION_LIST_RE); processInjections(xml, injections, ITEMIZEDLIST_INJECTION_POINT, CUSTOM_ALPHA_SORT_INJECTION_LIST_RE); processInjections(xml, injections, LIST_INJECTION_POINT, CUSTOM_INJECTION_LISTITEMS_RE); // Now make the custom injection point substitutions String fixedXML = xml; for (final Map.Entry<Integer, Map<String, List<InjectionData>>> entry : injections.entrySet()) { final Integer listType = entry.getKey(); final Map<String, List<InjectionData>> items = entry.getValue(); for (final Map.Entry<String, List<InjectionData>> typeEntry : items.entrySet()) { final String customInjectionComment = typeEntry.getKey(); String replacement = null; // Generate the dummy injection elements based on the type if (typeEntry.getValue() != null && typeEntry.getValue().size() > 0) { if (listType == ORDEREDLIST_INJECTION_POINT) { replacement = createDummyOrderedList(entityManager, xmlFormat, typeEntry.getValue(), hostUrl); } else if (listType == XREF_INJECTION_POINT) { replacement = createDummyXRef(entityManager, xmlFormat, typeEntry.getValue().get(0), hostUrl); } else if (listType == ITEMIZEDLIST_INJECTION_POINT) { replacement = createDummyItemizedList(entityManager, xmlFormat, typeEntry.getValue(), hostUrl); } else if (listType == LIST_INJECTION_POINT) { replacement = createDummyListItems(entityManager, xmlFormat, typeEntry.getValue(), hostUrl); } } // Substitute the dummy elements for the injection comment elements fixedXML = fixedXML.replace(customInjectionComment, replacement); } } return fixedXML; } /** * Process a XML Document to resolve any custom injection references so that they can be rendered/validated. This method will * transform the injections into links to the editor for each injected topic. It will not make any external calls, * so the text displayed will be "Topic <ID>". This also means that any injections that use sorting will not be sorted, * since we don't have access to the topic titles. * * @param doc The document to be processed. */ public static void resolveInjections(final EntityManager entityManager, final Integer xmlFormat, final Document doc, final String hostUrl) { // Make sure we have something to process. if (doc == null) { return; } // Find any comments that are injection references final List<Node> injectionComments = new ArrayList<Node>(); for (final Node comment : XMLUtilities.getComments(doc)) { if (comment.getNodeValue().matches("^\\s*Inject.*")) { injectionComments.add(comment); } } // Process the comments to get the injection references final Map<Integer, Map<Node, List<InjectionData>>> injections = new HashMap<Integer, Map<Node, List<InjectionData>>>(); processInjections(injectionComments, injections, ORDEREDLIST_INJECTION_POINT, DOC_CUSTOM_INJECTION_SEQUENCE_RE); processInjections(injectionComments, injections, XREF_INJECTION_POINT, DOC_CUSTOM_INJECTION_SINGLE_RE); processInjections(injectionComments, injections, ITEMIZEDLIST_INJECTION_POINT, DOC_CUSTOM_INJECTION_LIST_RE); processInjections(injectionComments, injections, ITEMIZEDLIST_INJECTION_POINT, DOC_CUSTOM_ALPHA_SORT_INJECTION_LIST_RE); processInjections(injectionComments, injections, LIST_INJECTION_POINT, DOC_CUSTOM_INJECTION_LISTITEMS_RE); // Now make the custom injection point substitutions for (final Map.Entry<Integer, Map<Node, List<InjectionData>>> entry : injections.entrySet()) { final Integer listType = entry.getKey(); final Map<Node, List<InjectionData>> items = entry.getValue(); for (final Map.Entry<Node, List<InjectionData>> typeEntry : items.entrySet()) { final Node customInjectionCommentNode = typeEntry.getKey(); List<Element> list = null; // Generate the dummy injection elements based on the type if (typeEntry.getValue() != null && typeEntry.getValue().size() > 0) { if (listType == ORDEREDLIST_INJECTION_POINT) { list = Arrays.asList(createDummyOrderedList(entityManager, xmlFormat, doc, typeEntry.getValue(), hostUrl)); } else if (listType == XREF_INJECTION_POINT) { list = createDummyXRef(entityManager, xmlFormat, doc, typeEntry.getValue().get(0), hostUrl); } else if (listType == ITEMIZEDLIST_INJECTION_POINT) { list = Arrays.asList(createDummyItemizedList(entityManager, xmlFormat, doc, typeEntry.getValue(), hostUrl)); } else if (listType == LIST_INJECTION_POINT) { list = createDummyListItems(entityManager, xmlFormat, doc, typeEntry.getValue(), hostUrl); } } // Substitute the dummy elements for the injection comment elements if (list != null) { for (final Element element : list) { customInjectionCommentNode.getParentNode().insertBefore(element, customInjectionCommentNode); } customInjectionCommentNode.getParentNode().removeChild(customInjectionCommentNode); } } } } /** * Processes a List of Comment elements to get any injection references. * * @param xml * @param injections A map that will have any processed injections added to. * @param injectionPointType The injection type that is being processed. * @param regularExpression The regular expression for the injection type. */ protected static void processInjections(final String xml, final Map<Integer, Map<String, List<InjectionData>>> injections, final Integer injectionPointType, final Pattern regularExpression) { // Create the mapping if it doesn't exist for the injection type if (!injections.containsKey(injectionPointType)) { injections.put(injectionPointType, new HashMap<String, List<InjectionData>>()); } // loop over all of the comments that were marked as injections Matcher matcher = regularExpression.matcher(xml); while (matcher.find()) { // Get the list of topics from the named group in the regular expression match final String reMatch = matcher.group(1); // Make sure we actually found something if (reMatch != null) { // Get the sequence of ids final List<InjectionData> injectionData = processIdList(reMatch); injections.get(injectionPointType).put(matcher.group(0), injectionData); } } } /** * Create a dummy xref representation that can be used for validation/rendering * * @param xmlFormat * @param injectionData The injected topic information to create the link for. * @param hostUrl The host url of the application, so an editor link can be constructed. * @return A List of Elements that make up the injected dummy link. */ protected static String createDummyXRef(final EntityManager entityManager, final Integer xmlFormat, final InjectionData injectionData, final String hostUrl) { final StringBuilder retValue = new StringBuilder(); if (injectionData.optional) { retValue.append("<emphasis>"); retValue.append(OPTIONAL_LIST_PREFIX); retValue.append("</emphasis>"); } final String url, title; if (injectionData.id.matches("^\\d+$")) { url = hostUrl.replace(HOST_URL_ID_TOKEN, injectionData.id); final Topic destinationTopic = entityManager.find(Topic.class, Integer.parseInt(injectionData.id)); title = destinationTopic.getTopicTitle(); } else { // TODO need to work out a way to link to a target url = hostUrl + "#"; title = "Target " + injectionData.id; } // Use a link instead of a xref because the xref target won't exist and therefore won't validate if (xmlFormat == CommonConstants.DOCBOOK_50) { retValue.append("<link xlink:href=\""); retValue.append(url); retValue.append("\">"); retValue.append(title); retValue.append("</link>"); } else { retValue.append("<ulink url=\""); retValue.append(url); retValue.append("\">"); retValue.append(title); retValue.append("</ulink>"); } return retValue.toString(); } /** * Create a dummy itemizedlist representation that can be used for validation/rendering * * @param xmlFormat * @param injectionDatas The list of injected topic information to create the list for. * @param hostUrl The host url of the application, so an editor link can be constructed for each topic. * @return The dummy itemized list representation. */ protected static String createDummyItemizedList(final EntityManager entityManager, final Integer xmlFormat, final List<InjectionData> injectionDatas, final String hostUrl) { final StringBuilder retValue = new StringBuilder("<para><itemizedlist>"); retValue.append(createDummyListItems(entityManager, xmlFormat, injectionDatas, hostUrl)); retValue.append("</itemizedlist></para>"); return retValue.toString(); } /** * Create a dummy orderedlist representation that can be used for validation/rendering * * @param xmlFormat * @param injectionDatas The list of injected topic information to create the list for. * @param hostUrl The host url of the application, so an editor link can be constructed for each topic. * @return The dummy ordered list representation. */ protected static String createDummyOrderedList(final EntityManager entityManager, final Integer xmlFormat, final List<InjectionData> injectionDatas, final String hostUrl) { final StringBuilder retValue = new StringBuilder("<para><orderedlist>"); retValue.append(createDummyListItems(entityManager, xmlFormat, injectionDatas, hostUrl)); retValue.append("</orderedlist></para>"); return retValue.toString(); } protected static String createDummyListItems(final EntityManager entityManager, final Integer xmlFormat, final List<InjectionData> injectionDatas, final String hostUrl) { final StringBuilder retValue = new StringBuilder(); for (final InjectionData topicData : injectionDatas) { retValue.append("<listitem><para>"); retValue.append(createDummyXRef(entityManager, xmlFormat, topicData, hostUrl)); retValue.append("</para></listitem>"); } return retValue.toString(); } /** * Processes a List of Comment elements to get any injection references. * * @param injectionNodes A list of Comment elements that are actually injection references * @param injections A map that will have any processed injections added to. * @param injectionPointType The injection type that is being processed. * @param regularExpression The regular expression for the injection type. */ protected static void processInjections(final List<Node> injectionNodes, final Map<Integer, Map<Node, List<InjectionData>>> injections, final Integer injectionPointType, final Pattern regularExpression) { // Create the mapping if it doesn't exist for the injection type if (!injections.containsKey(injectionPointType)) { injections.put(injectionPointType, new HashMap<Node, List<InjectionData>>()); } // loop over all of the comments that were marked as injections for (final Node comment : injectionNodes) { final String commentContent = comment.getNodeValue(); // find any matches final Matcher injectionMatchResult = regularExpression.matcher(commentContent); // If a match was found then extract the data if (injectionMatchResult.find()) { // Get the list of topics from the named group in the regular expression match final String reMatch = injectionMatchResult.group(1); // Make sure we actually found something if (reMatch != null) { // Get the sequence of ids final List<InjectionData> injectionData = processIdList(reMatch); injections.get(injectionPointType).put(comment, injectionData); } } } } /** * Create a dummy xref representation that can be used for validation/rendering * * @param xmlFormat * @param doc The DOM Document the dummy link should be created for. * @param injectionData The injected topic information to create the link for. * @param hostUrl The host url of the application, so an editor link can be constructed. * @return A List of Elements that make up the injected dummy link. */ protected static List<Element> createDummyXRef(final EntityManager entityManager, final Integer xmlFormat, final Document doc, final InjectionData injectionData, final String hostUrl) { final List<Element> retValue = new ArrayList<Element>(); if (injectionData.optional) { final Element emphasis = doc.createElement("emphasis"); emphasis.appendChild(doc.createTextNode(OPTIONAL_LIST_PREFIX)); retValue.add(emphasis); } final String url, title; if (injectionData.id.matches("^\\d+$")) { url = hostUrl.replace(HOST_URL_ID_TOKEN, injectionData.id); final Topic destinationTopic = entityManager.find(Topic.class, Integer.parseInt(injectionData.id)); title = destinationTopic.getTopicTitle(); } else { // TODO need to work out a way to link to a target url = hostUrl + "#"; title = "Target " + injectionData.id; } // Use a link instead of a xref because the xref target won't exist and therefore won't validate final Element xRef; if (xmlFormat == CommonConstants.DOCBOOK_50) { xRef= doc.createElement("link"); xRef.setAttribute("xlink:href", url); } else { xRef= doc.createElement("ulink"); xRef.setAttribute("url", url); } xRef.appendChild(doc.createTextNode(title)); retValue.add(xRef); return retValue; } /** * Create a dummy itemizedlist representation that can be used for validation/rendering * * @param xmlFormat * @param doc The DOM Document the dummy link should be created for. * @param injectionDatas The list of injected topic information to create the list for. * @param hostUrl The host url of the application, so an editor link can be constructed for each topic. * @return The dummy itemized list representation. */ protected static Element createDummyItemizedList(final EntityManager entityManager, final Integer xmlFormat, final Document doc, final List<InjectionData> injectionDatas, final String hostUrl) { final Element para = doc.createElement("para"); final Element itemizedList = doc.createElement("itemizedlist"); para.appendChild(itemizedList); final List<Element> listItems = createDummyListItems(entityManager, xmlFormat, doc, injectionDatas, hostUrl); for (final Element listItem : listItems) { itemizedList.appendChild(listItem); } return para; } /** * Create a dummy orderedlist representation that can be used for validation/rendering * * @param xmlFormat * @param doc The DOM Document the dummy link should be created for. * @param injectionDatas The list of injected topic information to create the list for. * @param hostUrl The host url of the application, so an editor link can be constructed for each topic. * @return The dummy ordered list representation. */ protected static Element createDummyOrderedList(final EntityManager entityManager, final Integer xmlFormat, final Document doc, final List<InjectionData> injectionDatas, final String hostUrl) { final Element para = doc.createElement("para"); final Element orderedList = doc.createElement("orderedlist"); para.appendChild(orderedList); final List<Element> listItems = createDummyListItems(entityManager, xmlFormat, doc, injectionDatas, hostUrl); for (final Element listItem : listItems) { orderedList.appendChild(listItem); } return para; } protected static List<Element> createDummyListItems(final EntityManager entityManager, final Integer xmlFormat, final Document doc, final List<InjectionData> injectionDatas, final String hostUrl) { final List<Element> retValue = new ArrayList<Element>(); for (final InjectionData topicData : injectionDatas) { final Element listitem = doc.createElement("listitem"); retValue.add(listitem); final Element listItemPara = doc.createElement("para"); listitem.appendChild(listItemPara); final List<Element> elements = createDummyXRef(entityManager, xmlFormat, doc, topicData, hostUrl); for (final Element ele : elements) { listItemPara.appendChild(ele); } } return retValue; } /** * Takes a comma separated list of optional topic references, and returns an array of InjectionData. * * @param list * @return */ protected static List<InjectionData> processIdList(final String list) { // Find the individual topic ids final String[] ids = list.split(","); final List<InjectionData> retValue = new ArrayList<InjectionData>(ids.length); // Clean the topic ids for (final String topicId : ids) { final String id = topicId.replaceAll(OPTIONAL_MARKER, "").trim(); final boolean optional = topicId.contains(OPTIONAL_MARKER); retValue.add(new InjectionData(id, optional)); } return retValue; } /** * A Class to hold information about a injection reference. */ protected static class InjectionData { /** * The topic/target ID */ public String id; /** * whether this topic was marked as optional */ public boolean optional; public InjectionData(final String id, final boolean optional) { this.id = id; this.optional = optional; } } }