001/* 002 * The contents of this file are subject to the terms of the Common Development and 003 * Distribution License (the License). You may not use this file except in compliance with the 004 * License. 005 * 006 * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the 007 * specific language governing permission and limitations under the License. 008 * 009 * When distributing Covered Software, include this CDDL Header Notice in each file and include 010 * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL 011 * Header, with the fields enclosed by brackets [] replaced by your own identifying 012 * information: "Portions copyright [year] [name of copyright owner]". 013 * 014 * Copyright 2015 ForgeRock AS. 015 */ 016 017package org.forgerock.doc.maven.utils; 018 019import org.forgerock.doc.maven.utils.helper.FileFilterFactory; 020 021import java.io.File; 022import java.io.FileFilter; 023import java.io.IOException; 024import java.util.Map; 025 026/** 027 * Offers an alternative to <a href="http://www.sagehill.net/docbookxsl/Profiling.html">DocBook profiling</a>. 028 * <br> 029 * docbkx-tools relies on support in the DocBook XSL distribution, 030 * which does not appear to include profiling support for webhelp output (v1.78.1). 031 * docbkx-tools also handles the profiling in the same pass as the build, 032 * meaning that dangling links might result in the built output. 033 * <br> 034 * This implementation can be used to handle profiling (conditional text) during pre-processing. 035 * Build tools and validators then need not deal with DocBook profiles. 036 */ 037public class Profiler { 038 039 /** Profile maps: keys == attr names; values == attr values. **/ 040 private final Map<String, String> inclusions; 041 private final Map<String, String> exclusions; 042 043 /** 044 * Constructs a profiler based on a profile inclusions configuration. 045 * <br> 046 * The profile maps do not restrict the keys (attribute names) to those supported by DocBook profiling. 047 * 048 * @param inclusions Profile map: keys == attr names; values == attr values. 049 * Lists of attr values must be space-separated. 050 * If no inclusions are specified, set this to null. 051 * @param exclusions Profile map: keys == attr names; values == attr values. 052 * Lists of attr values must be space-separated. 053 * If no exclusions are specified, set this to null. 054 */ 055 public Profiler(final Map<String, String> inclusions, final Map<String, String> exclusions) { 056 this.inclusions = inclusions; 057 this.exclusions = exclusions; 058 } 059 060 /** 061 * Applies inclusions and exclusions if any to the XML files under the source directory. 062 * <br> 063 * This method changes the files in place, so the source should be a modifiable copy. 064 * 065 * @param xmlSourceDirectory Source directory for XML files to profile. 066 * @throws IOException Failed to transform an XML file. 067 */ 068 public void applyProfiles(final File xmlSourceDirectory) throws IOException { 069 if (inclusions == null && exclusions == null) { // Nothing to do. 070 return; 071 } 072 073 final FileFilter fileFilter = FileFilterFactory.getXmlFileFilter(); 074 Transformer transformer = new Transformer(getXsl(), fileFilter); 075 transformer.update(xmlSourceDirectory); 076 } 077 078 /** 079 * Returns an XSLT stylesheet suitable for profiling. 080 * <br> 081 * If no inclusions or exclusions are set, this makes a copy (identity transform). 082 * 083 * @return An XSLT stylesheet suitable for profiling. 084 */ 085 private String getXsl() { 086 final StringBuilder sb = new StringBuilder(); 087 sb.append("<?xml version=\"1.0\"?>\n") 088 .append("<xsl:stylesheet version=\"1.0\" xmlns:xsl=\"http://www.w3.org/1999/XSL/Transform\">\n") 089 .append(" <xsl:template match=\"node()|@*\">\n") 090 .append(" <xsl:copy>\n") 091 .append(" <xsl:apply-templates select=\"node()|@*\"/>\n") 092 .append(" </xsl:copy>\n") 093 .append(" </xsl:template>\n"); 094 095 if (inclusions != null) { 096 for (final String attribute : inclusions.keySet()) { 097 sb.append("<xsl:template match=\"//*[") 098 .append(getInclusionsMatch(attribute, inclusions.get(attribute).split("\\s+"))) 099 .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}