View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    *
9    *      https://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  package org.apache.commons.text.translate;
18  
19  import java.io.IOException;
20  import java.io.Writer;
21  import java.util.Arrays;
22  import java.util.Collections;
23  import java.util.EnumSet;
24  
25  import org.apache.commons.lang3.ArrayUtils;
26  import org.apache.commons.lang3.CharUtils;
27  
28  /**
29   * Translates XML numeric entities of the form &#[xX]?\d+;? to
30   * the specific code point.
31   *
32   * Note that the semicolon is optional.
33   *
34   * @since 1.0
35   */
36  public class NumericEntityUnescaper extends CharSequenceTranslator {
37  
38      /**
39       * Enumerates NumericEntityUnescaper options for unescaping.
40       */
41      public enum OPTION {
42  
43          /**
44           * Requires a semicolon.
45           */
46          semiColonRequired,
47  
48          /**
49           * Does not require a semicolon.
50           */
51          semiColonOptional,
52  
53          /**
54           * Throws an exception if a semicolon is missing.
55           */
56          errorIfNoSemiColon
57      }
58  
59      /** Default options. */
60      private static final EnumSet<OPTION> DEFAULT_OPTIONS = EnumSet
61          .copyOf(Collections.singletonList(OPTION.semiColonRequired));
62  
63      /** EnumSet of OPTIONS, given from the constructor, read-only. */
64      private final EnumSet<OPTION> options;
65  
66      /**
67       * Constructs a new instance.
68       *
69       * The constructor takes a list of options, only one type of which is currently available (whether to allow, error or ignore the semicolon on the end of a
70       * numeric entity to being missing).
71       * <p>
72       * For example, to support numeric entities without a ';':
73       * </p>
74       *
75       * <pre>
76       * new NumericEntityUnescaper(NumericEntityUnescaper.OPTION.semiColonOptional)
77       * </pre>
78       * <p>
79       * and to throw an IllegalArgumentException when they're missing:
80       * </p>
81       *
82       * <pre>
83       * new NumericEntityUnescaper(NumericEntityUnescaper.OPTION.errorIfNoSemiColon)
84       * </pre>
85       * <p>
86       * Note that the default behavior is to ignore them.
87       * </p>
88       *
89       * @param options to apply to this unescaper
90       */
91      public NumericEntityUnescaper(final OPTION... options) {
92          this.options = ArrayUtils.isEmpty(options) ? DEFAULT_OPTIONS : EnumSet.copyOf(Arrays.asList(options));
93      }
94  
95      /**
96       * Tests whether the passed in option is currently set.
97       *
98       * @param option to check state of.
99       * @return whether the option is set.
100      */
101     public boolean isSet(final OPTION option) {
102         return options.contains(option);
103     }
104 
105     /**
106      * {@inheritDoc}
107      */
108     @Override
109     public int translate(final CharSequence input, final int index, final Writer writer) throws IOException {
110         final int seqEnd = input.length();
111         // Uses -2 to ensure there is something after the &#
112         if (input.charAt(index) == '&' && index < seqEnd - 2 && input.charAt(index + 1) == '#') {
113             int start = index + 2;
114             boolean isHex = false;
115             final char firstChar = input.charAt(start);
116             if (firstChar == 'x' || firstChar == 'X') {
117                 start++;
118                 isHex = true;
119                 // Check there's more than just an x after the &#
120                 if (start == seqEnd) {
121                     return 0;
122                 }
123             }
124             int end = start;
125             // Note that this supports character codes without a ; on the end
126             while (end < seqEnd && CharUtils.isHex(input.charAt(end))) {
127                 end++;
128             }
129             final boolean semiNext = end != seqEnd && input.charAt(end) == ';';
130             if (!semiNext) {
131                 if (isSet(OPTION.semiColonRequired)) {
132                     return 0;
133                 }
134                 if (isSet(OPTION.errorIfNoSemiColon)) {
135                     throw new IllegalArgumentException("Semi-colon required at end of numeric entity");
136                 }
137             }
138             final int entityValue;
139             try {
140                 if (isHex) {
141                     entityValue = Integer.parseInt(input.subSequence(start, end).toString(), 16);
142                 } else {
143                     entityValue = Integer.parseInt(input.subSequence(start, end).toString(), 10);
144                 }
145             } catch (final NumberFormatException nfe) {
146                 return 0;
147             }
148             if (!Character.isValidCodePoint(entityValue)) {
149                 return 0;
150             }
151             if (entityValue > 0xFFFF) {
152                 final char[] chars = Character.toChars(entityValue);
153                 writer.write(chars[0]);
154                 writer.write(chars[1]);
155             } else {
156                 writer.write(entityValue);
157             }
158             return 2 + end - start + (isHex ? 1 : 0) + (semiNext ? 1 : 0);
159         }
160         return 0;
161     }
162 }