View Javadoc
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 }