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}