1 /* 2 * The contents of this file are subject to the terms of the Common Development and 3 * Distribution License (the License). You may not use this file except in compliance with the 4 * License. 5 * 6 * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the 7 * specific language governing permission and limitations under the License. 8 * 9 * When distributing Covered Software, include this CDDL Header Notice in each file and include 10 * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL 11 * Header, with the fields enclosed by brackets [] replaced by your own identifying 12 * information: "Portions copyright [year] [name of copyright owner]". 13 * 14 * Copyright 2015 ForgeRock AS. 15 */ 16 17 package org.forgerock.doc.maven.utils; 18 19 import org.forgerock.doc.maven.utils.helper.FileFilterFactory; 20 21 import java.io.File; 22 import java.io.FileFilter; 23 import java.io.IOException; 24 import java.util.Map; 25 26 /** 27 * Offers an alternative to <a href="http://www.sagehill.net/docbookxsl/Profiling.html">DocBook profiling</a>. 28 * <br> 29 * docbkx-tools relies on support in the DocBook XSL distribution, 30 * which does not appear to include profiling support for webhelp output (v1.78.1). 31 * docbkx-tools also handles the profiling in the same pass as the build, 32 * meaning that dangling links might result in the built output. 33 * <br> 34 * This implementation can be used to handle profiling (conditional text) during pre-processing. 35 * Build tools and validators then need not deal with DocBook profiles. 36 */ 37 public class Profiler { 38 39 /** Profile maps: keys == attr names; values == attr values. **/ 40 private final Map<String, String> inclusions; 41 private final Map<String, String> exclusions; 42 43 /** 44 * Constructs a profiler based on a profile inclusions configuration. 45 * <br> 46 * The profile maps do not restrict the keys (attribute names) to those supported by DocBook profiling. 47 * 48 * @param inclusions Profile map: keys == attr names; values == attr values. 49 * Lists of attr values must be space-separated. 50 * If no inclusions are specified, set this to null. 51 * @param exclusions Profile map: keys == attr names; values == attr values. 52 * Lists of attr values must be space-separated. 53 * If no exclusions are specified, set this to null. 54 */ 55 public Profiler(final Map<String, String> inclusions, final Map<String, String> exclusions) { 56 this.inclusions = inclusions; 57 this.exclusions = exclusions; 58 } 59 60 /** 61 * Applies inclusions and exclusions if any to the XML files under the source directory. 62 * <br> 63 * This method changes the files in place, so the source should be a modifiable copy. 64 * 65 * @param xmlSourceDirectory Source directory for XML files to profile. 66 * @throws IOException Failed to transform an XML file. 67 */ 68 public void applyProfiles(final File xmlSourceDirectory) throws IOException { 69 if (inclusions == null && exclusions == null) { // Nothing to do. 70 return; 71 } 72 73 final FileFilter fileFilter = FileFilterFactory.getXmlFileFilter(); 74 Transformer transformer = new Transformer(getXsl(), fileFilter); 75 transformer.update(xmlSourceDirectory); 76 } 77 78 /** 79 * Returns an XSLT stylesheet suitable for profiling. 80 * <br> 81 * If no inclusions or exclusions are set, this makes a copy (identity transform). 82 * 83 * @return An XSLT stylesheet suitable for profiling. 84 */ 85 private String getXsl() { 86 final StringBuilder sb = new StringBuilder(); 87 sb.append("<?xml version=\"1.0\"?>\n") 88 .append("<xsl:stylesheet version=\"1.0\" xmlns:xsl=\"http://www.w3.org/1999/XSL/Transform\">\n") 89 .append(" <xsl:template match=\"node()|@*\">\n") 90 .append(" <xsl:copy>\n") 91 .append(" <xsl:apply-templates select=\"node()|@*\"/>\n") 92 .append(" </xsl:copy>\n") 93 .append(" </xsl:template>\n"); 94 95 if (inclusions != null) { 96 for (final String attribute : inclusions.keySet()) { 97 sb.append("<xsl:template match=\"//*[") 98 .append(getInclusionsMatch(attribute, inclusions.get(attribute).split("\\s+"))) 99 .append("]\"/>\n"); 100 } 101 } 102 103 if (exclusions != null) { 104 for (final String attribute : exclusions.keySet()) { 105 sb.append("<xsl:template match=\"//*[") 106 .append(getExclusionsMatch(attribute, exclusions.get(attribute).split("\\s+"))) 107 .append("]\"/>\n"); 108 } 109 } 110 111 sb.append("</xsl:stylesheet>\n"); 112 return sb.toString(); 113 } 114 115 /** 116 * Returns a partial XPath expression to match inclusions. 117 * <br> 118 * This follows a sort of reversed logic to <em>exclude non-matching elements</em>: 119 * <br> 120 * attr "os", values { "linux", "unix" } results in "@os != 'linux' and @os != 'unix'" 121 * <br> 122 * attr "condition", values { "release" } results in "@condition != 'release'" 123 * 124 * @param attribute The attribute for which to include elements with matching values. 125 * @param values The attribute values to match for inclusion. 126 * @return A partial XPath expression to match inclusions. 127 */ 128 private String getInclusionsMatch(final String attribute, final String[] values) { 129 if (attribute == null || attribute.isEmpty()) { 130 return ""; 131 } 132 133 if (values == null || values.length == 0) { 134 return ""; 135 } 136 137 if (values.length == 1) { 138 return "@" + attribute + " != '" + values[0] + "'"; 139 } else { 140 StringBuilder sb = new StringBuilder(); 141 for (int i = values.length - 1; i > 0; i--) { 142 sb.append("@").append(attribute).append(" != '").append(values[i]).append("' and "); 143 } 144 sb.append("@").append(attribute).append(" != '").append(values[0]).append("'"); 145 return sb.toString(); 146 } 147 } 148 149 /** 150 * Returns a partial XPath expression to match exclusions. 151 * <br> 152 * attr "os", values { "linux", "unix" } results in "@os = 'linux' or @os = 'unix'" 153 * <br> 154 * attr "condition", values { "draft" } results in "@condition = 'draft'" 155 * 156 * @param attribute The attribute for which to exclude elements with matching values. 157 * @param values The attribute values to match for exclusion. 158 * @return A partial XPath expression to match exclusions. 159 */ 160 private String getExclusionsMatch(final String attribute, final String[] values) { 161 if (attribute == null || attribute.isEmpty()) { 162 return ""; 163 } 164 165 if (values == null || values.length == 0) { 166 return ""; 167 } 168 169 if (values.length == 1) { 170 return "@" + attribute + " = '" + values[0] + "'"; 171 } else { 172 StringBuilder sb = new StringBuilder(); 173 for (int i = values.length - 1; i > 0; i--) { 174 sb.append("@").append(attribute).append(" = '").append(values[i]).append("' or "); 175 } 176 sb.append("@").append(attribute).append(" = '").append(values[0]).append("'"); 177 return sb.toString(); 178 } 179 } 180 181 /** 182 * Applies an XSL transformation to the matching files. 183 */ 184 private class Transformer extends XmlTransformer { 185 /** 186 * Constructs an updater to match DocBook XML files. 187 * <br> 188 * The files are updated in place. 189 * 190 * @param xsl XSL as a String. 191 * @param filterToMatch Filter to match XML files. 192 */ 193 public Transformer(String xsl, FileFilter filterToMatch) { 194 super(xsl, filterToMatch); 195 } 196 } 197 }