1   package nl.dedicon.pipeline.braille.step;
2   
3   import java.io.File;
4   import java.io.IOException;
5   import java.io.StringReader;
6   import java.nio.file.Files;
7   import javax.xml.parsers.DocumentBuilderFactory;
8   import javax.xml.parsers.ParserConfigurationException;
9   import javax.xml.transform.stream.StreamSource;
10  import net.sf.saxon.s9api.Axis;
11  import net.sf.saxon.s9api.DocumentBuilder;
12  import net.sf.saxon.s9api.QName;
13  import net.sf.saxon.s9api.SaxonApiException;
14  import net.sf.saxon.s9api.XdmNode;
15  import net.sf.saxon.s9api.XdmSequenceIterator;
16  import org.w3c.dom.Document;
17  import org.w3c.dom.Element;
18  import org.w3c.dom.Node;
19  import org.xml.sax.InputSource;
20  import org.xml.sax.SAXException;
21  
22  /**
23   * Utilities for Java XProc steps
24   * 
25   * @author Paul Rambags
26   */
27  public class Utils  {
28      
29      /**
30       * Convert an XdmNode to a Document node
31       * 
32       * @param node XdmNode
33       * @return Document node
34       * @throws ParserConfigurationException thrown on parse exceptions
35       * @throws SAXException thrown on a SAX exception
36       * @throws IOException thrown on an IO exception
37       */
38      public static Document convertToDocument(XdmNode node) throws ParserConfigurationException, SAXException, IOException {
39          return convertToDocument(node.toString());
40      }
41      
42      /**
43       * Convert an XML string to a Document node
44       * 
45       * @param xml XML string
46       * @return Document node
47       * @throws ParserConfigurationException thrown on parse exceptions
48       * @throws SAXException thrown on a SAX exception
49       * @throws IOException thrown on an IO exception
50       */
51      public static Document convertToDocument(String xml) throws ParserConfigurationException, SAXException, IOException {
52          DocumentBuilderFactory builderFactory = DocumentBuilderFactory.newInstance();
53          builderFactory.setNamespaceAware(true);
54          javax.xml.parsers.DocumentBuilder documentBuilder = builderFactory.newDocumentBuilder();
55          Document document = documentBuilder.parse(new InputSource(new StringReader(xml)));
56          return document;
57      }
58      
59      /**
60       * Convert a Document node to an XdmNode
61       * 
62       * @param document Document node
63       * @param documentBuilder document builder
64       * @param useFile if true, the resulting XdmNode will be saved in a temporary file so that it has an URI
65       * @return XdmNode
66       * @throws IOException thrown on an IO exception
67       * @throws SaxonApiException thrown on a Saxon API exception
68       */
69      public static XdmNode convertToXdmNode(Document document, DocumentBuilder documentBuilder, boolean useFile) throws IOException, SaxonApiException {
70          String xml = toString(document, documentBuilder);
71          XdmNode node;
72          if (useFile) {
73              String className = new Object(){}.getClass().getName();
74              File tempFile = File.createTempFile(className, ".tmp");
75              tempFile.deleteOnExit();
76              Files.write(tempFile.toPath(), xml.getBytes());
77              node = documentBuilder.build(tempFile);
78          } else {
79              node = documentBuilder.build(new StreamSource(new StringReader(xml)));
80          }
81          return node;
82      }
83  
84      /**
85       * Convert a Document node to a string
86       * 
87       * @param document Document node
88       * @param documentBuilder document builder
89       * @return XML string
90       */
91      public static String toString(Document document, DocumentBuilder documentBuilder) {
92          return documentBuilder.wrap(document).toString();
93      }
94  
95      /**
96       * Get the first child in the same namespace with a given name
97       * 
98       * @param node node
99       * @param name child node name
100      * @return the first child in the same namespace with the given name, or null if it doesn't exist
101      */
102     public static Node getChild(Node node, String name) {
103         if (node == null) {
104             return null;
105         }
106         String namespace = node.getNamespaceURI();
107         if (namespace == null) {
108             return null;
109         }
110         Node child = node.getFirstChild();
111         while (child != null) {
112             if (namespace.equals(child.getNamespaceURI()) && name.equals(child.getNodeName())) {
113                 return child;
114             }
115             child = child.getNextSibling();
116         }
117         return null;
118     }
119     
120     /**
121      * Get the first child in the same namespace with a given name and a given attribute value
122      * 
123      * @param node node
124      * @param name child node name
125      * @param attributeName attribute name
126      * @param attributeValue attribute value
127      * @return the first child in the same namespace with the given name and with the given attribute value, or null if it doesn't exist
128      */
129     public static Node getFirstChildWithAttribute(Node node, String name, String attributeName, String attributeValue) {
130         if (node == null) {
131             return null;
132         }
133         String namespace = node.getNamespaceURI();
134         if (namespace == null) {
135             return null;
136         }
137         Node child = node.getFirstChild();
138         while (child != null) {
139             if (namespace.equals(child.getNamespaceURI()) &&
140                     name.equals(child.getNodeName()) &&
141                     child.getNodeType() == Node.ELEMENT_NODE &&
142                     attributeValue.equals(((Element)child).getAttribute(attributeName))) {
143                 return child;
144             }
145             child = child.getNextSibling();
146         }
147         return null;
148     }
149     
150     /**
151      * Get the first sibling in the same namespace with a given name and a given attribute value
152      * 
153      * @param node node
154      * @param name sibling node name
155      * @param attributeName attribute name
156      * @param attributeValue attribute value
157      * @return the first sibling in the same namespace with the given name and with the given attribute value, or null if it doesn't exist
158      */
159     public static Node getNextSiblingWithAttribute(Node node, String name, String attributeName, String attributeValue) {
160         if (node == null) {
161             return null;
162         }
163         String namespace = node.getNamespaceURI();
164         if (namespace == null) {
165             return null;
166         }
167         Node child = node.getNextSibling();
168         while (child != null) {
169             if (namespace.equals(child.getNamespaceURI()) &&
170                     name.equals(child.getNodeName()) &&
171                     child.getNodeType() == Node.ELEMENT_NODE &&
172                     attributeValue.equals(((Element)child).getAttribute(attributeName))) {
173                 return child;
174             }
175             child = child.getNextSibling();
176         }
177         return null;
178     }
179     
180     /**
181      * Add a new child node to a parent node
182      * The new child node will inherit the parent's namespace
183      * 
184      * @param parent parent node
185      * @param name name of the child node
186      * @return the new child node
187      */
188     public static Element addChild(Node parent, String name) {
189         Element child = parent.getOwnerDocument().createElementNS(parent.getNamespaceURI(), name);
190         parent.appendChild(child);
191         return child;
192     }
193     
194     /**
195      * Add a new child node to a parent node before a child node
196      * The new child node will inherit the parent's namespace
197      * If the child node is null, the new child node will be added at the end of the list of children
198      * 
199      * @param parent parent node
200      * @param child child node, can be null
201      * @param name name of the child node
202      * @return the new child node
203      */
204     public static Element addChildBefore(Node parent, Node child, String name) {
205         Element newChild = parent.getOwnerDocument().createElementNS(parent.getNamespaceURI(), name);
206         parent.insertBefore(newChild, child);
207         return newChild;
208     }
209     
210     /**
211      * Rename a node and maintain the namespace
212      * 
213      * @param node the node that will get a new node name
214      * @param newNodeName the new node name
215      * @return the renamed node
216      */
217     public static Node renameNode(Node node, String newNodeName) {
218         Document document = node.getOwnerDocument();
219         String namespace = document.getDocumentElement().getNamespaceURI();
220         return document.renameNode(node, namespace, newNodeName);
221     }
222     
223     /**
224      * Get the first child node
225      * 
226      * @param node XdmNode
227      * @param childName child name
228      * @return XdmNode the first child node, or null if it doesn't exist
229      */
230     public static XdmNode getChildNode(XdmNode node, QName childName) {
231         XdmSequenceIterator childIterator = node.axisIterator(Axis.CHILD, childName);
232         if (!childIterator.hasNext()) {
233             return null;
234         }
235         return (XdmNode)childIterator.next();
236     }
237     
238     /**
239      * Get the string value of a child node
240      * 
241      * @param node XdmNode
242      * @param childName child name
243      * @return String value of the child, or null if it doesn't exist
244      */
245     public static String getValue(XdmNode node, QName childName) {
246         XdmNode childNode = getChildNode(node, childName);
247         return childNode != null ? childNode.getStringValue() : null;
248     }
249     
250     /**
251      * Converts XML end tags to a HTML end tags
252      * 
253      * Each "/>" is replaced by "></tag>" for the correct tag
254      * 
255      * @param xml XML string
256      * @return HTML string
257      */
258 
259     public static String convertXmlEndtagsToHtmlEndtags (String xml) {
260         return xml.replaceAll("<\\s*([^\\s>]+)([^>]*)/\\s*>", "<$1$2></$1>");
261     }
262     
263     /**
264      * Determine whether a unicode character is in the braille range
265      * 
266      * @param c unicode character
267      * @return true if it is in the braile range, false otherwise
268      */
269     public static boolean isBraille(char c) {
270         return 0x2800 <= c && c <= 0x283F;
271     }
272     
273     /**
274      * Convert a character from braille to lowercase text
275      * 
276      * Braille control characters are not changed
277      * For characters with more than one text representation,
278      * the most common one is chosen
279      * e.g. (123456) is displayed as é but can also be %
280      * 
281      * @param braille Braille character
282      * @return text character
283      */
284     public static char convertBraille(char braille) {
285         if (!isBraille(braille)) {
286             return 0x0000;
287         }
288         return " a,b'k;l⠈cif/msp⠐e:h*o!r⠘djg@ntq⠠\\?ê-u(v⠨îöë§xè&⠰û.ü)z\"[⠸ôwï⠼y]é".charAt(braille - 0x2800);
289     }
290 
291     /**
292      * Determine whether a braille character is a digit
293      * 
294      * @param braille braille character
295      * @return true if it is a digit in the braille range
296      */
297     public static boolean isBrailleDigit(char braille) {
298         return isDigit(convertBrailleNumeric(braille));
299     }
300     
301     /**
302      * Determine whether a character is a digit
303      * 
304      * @param c character
305      * @return true if it is a digit
306      */
307     public static boolean isDigit (char c) {
308         return '0' <= c && c <= '9';
309     }
310     
311     /**
312      * Convert a numeric character from braille to lowercase text
313      * 
314      * @param braille Braille character
315      * @return numeric text character
316      */
317     public static char convertBrailleNumeric(char braille) {
318         if (isBraille(braille)) {
319             char converted = convertBraille(braille);
320             if ('a' <= converted && converted <= 'j') {
321                 return "1234567890".charAt(converted - 'a');
322             }
323             return converted;
324         }
325         return 0x0000;
326     }
327     
328     /**
329      * Determine whether a character is the minus sign
330      * 
331      * @param braille braille character
332      * @return true when the character is '-'
333      */
334     public static boolean isBrailleMinus(char braille) {
335         char converted = convertBraille(braille);
336         return converted == '-';
337     }
338 
339     /**
340      * Determine whether a braille character is a decimal separator
341      * 
342      * @param braille braille character
343      * @return true when the braille character is a comma or period
344      */
345     public static boolean isBrailleDecimalSeparator(char braille) {
346         return isDecimalSeparator(convertBraille(braille));
347     }
348     
349     /**
350      * Determine whether a character is a decimal separator
351      * 
352      * @param c character
353      * @return true when the character is a comma or period
354      */
355     public static boolean isDecimalSeparator(char c) {
356         return c == '.' || c == ',';
357     }
358 
359     /**
360      * Determine whether a braille character can be used as a shorthand for 00 at the end of a price
361      * E.g. the = sign in this price: € 6,=
362      * 
363      * @param braille braille character
364      * @return true when the character is a - or a =
365      */
366     public static boolean isBrailleMoneyZeros(char braille) {
367         return isMoneyZeros(convertBraille(braille));
368     }
369 
370     /**
371      * Determine whether a character can be used as a shorthand for 00 at the end of a price
372      * E.g. the = sign in this price: € 6,=
373      * 
374      * @param c character
375      * @return true when the character is a - or a =
376      */
377     public static boolean isMoneyZeros(char c) {
378         return c == '-' || c == '=';
379     }
380 }