    001/*
    002 * Licensed to the Apache Software Foundation (ASF) under one or more
    003 * contributor license agreements.  See the NOTICE file distributed with
    004 * this work for additional information regarding copyright ownership.
    005 * The ASF licenses this file to You under the Apache License, Version 2.0
    006 * (the "License"); you may not use this file except in compliance with
    007 * the License.  You may obtain a copy of the License at
    008 *
    009 *      https://www.apache.org/licenses/LICENSE-2.0
    010 *
    011 * Unless required by applicable law or agreed to in writing, software
    012 * distributed under the License is distributed on an "AS IS" BASIS,
    013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    014 * See the License for the specific language governing permissions and
    015 * limitations under the License.
    016 */
    017package org.apache.commons.lang3.text.translate;
    018
    019import java.io.IOException;
    020import java.io.Writer;
    021import java.util.Arrays;
    022import java.util.Collections;
    023import java.util.EnumSet;
    024
    025import org.apache.commons.lang3.CharUtils;
    026
    027/**
    028 * Translate XML numeric entities of the form &amp;#[xX]?\d+;? to the specific code point.
    029 *
    030 * Note that the semicolon is optional.
    031 *
    032 * @since 3.0
    033 * @deprecated As of <a href="https://commons.apache.org/proper/commons-lang/changes-report.html#a3.6">3.6</a>, use Apache Commons Text
    034 *             <a href="https://commons.apache.org/proper/commons-text/javadocs/api-release/org/apache/commons/text/translate/NumericEntityUnescaper.html">
    035 *             NumericEntityUnescaper</a>.
    036 */
    037@Deprecated
    038public class NumericEntityUnescaper extends CharSequenceTranslator {
    039
    040    /**
    041     * Enumerates NumericEntityUnescaper options for unescaping.
    042     *
    043     * @deprecated As of 3.18.0, use Apache Commons Text <a href=
    044     *             "https://commons.apache.org/proper/commons-text/javadocs/api-release/org/apache/commons/text/translate/NumericEntityUnescaper.OPTION.html">
    045     *             NumericEntityUnescaper.OPTION</a>.
    046     */
    047    @Deprecated
    048    public enum OPTION {
    049
    050        /**
    051         * Require a semicolon.
    052         */
    053        semiColonRequired,
    054
    055        /**
    056         * Do not require a semicolon.
    057         */
    058        semiColonOptional,
    059
    060        /**
    061         * Throw an exception if a semicolon is missing.
    062         */
    063        errorIfNoSemiColon
    064    }
    065
    066    // TODO?: Create an OptionsSet class to hide some of the conditional logic below
    067    private final EnumSet<OPTION> options;
    068
    069    /**
    070     * Create a UnicodeUnescaper.
    071     *
    072     * The constructor takes a list of options, only one type of which is currently
    073     * available (whether to allow, error or ignore the semicolon on the end of a
    074     * numeric entity to being missing).
    075     *
    076     * For example, to support numeric entities without a ';':
    077     *    new NumericEntityUnescaper(NumericEntityUnescaper.OPTION.semiColonOptional)
    078     * and to throw an IllegalArgumentException when they're missing:
    079     *    new NumericEntityUnescaper(NumericEntityUnescaper.OPTION.errorIfNoSemiColon)
    080     *
    081     * Note that the default behavior is to ignore them.
    082     *
    083     * @param options to apply to this unescaper.
    084     */
    085    public NumericEntityUnescaper(final OPTION... options) {
    086        if (options.length > 0) {
    087            this.options = EnumSet.copyOf(Arrays.asList(options));
    088        } else {
    089            this.options = EnumSet.copyOf(Collections.singletonList(OPTION.semiColonRequired));
    090        }
    091    }
    092
    093    /**
    094     * Tests whether the passed in option is currently set.
    095     *
    096     * @param option to check state of.
    097     * @return whether the option is set.
    098     */
    099    public boolean isSet(final OPTION option) {
    100        return options != null && options.contains(option);
    101    }
    102
    103    /**
    104     * {@inheritDoc}
    105     */
    106    @Override
    107    public int translate(final CharSequence input, final int index, final Writer out) throws IOException {
    108        final int seqEnd = input.length();
    109        // Uses -2 to ensure there is something after the &#
    110        if (input.charAt(index) == '&' && index < seqEnd - 2 && input.charAt(index + 1) == '#') {
    111            int start = index + 2;
    112            boolean isHex = false;
    113
    114            final char firstChar = input.charAt(start);
    115            if (firstChar == 'x' || firstChar == 'X') {
    116                start++;
    117                isHex = true;
    118
    119                // Check there's more than just an x after the &#
    120                if (start == seqEnd) {
    121                    return 0;
    122                }
    123            }
    124
    125            int end = start;
    126            // Note that this supports character codes without a ; on the end
    127            while (end < seqEnd && CharUtils.isHex(input.charAt(end))) {
    128                end++;
    129            }
    130
    131            final boolean semiNext = end != seqEnd && input.charAt(end) == ';';
    132
    133            if (!semiNext) {
    134                if (isSet(OPTION.semiColonRequired)) {
    135                    return 0;
    136                }
    137                if (isSet(OPTION.errorIfNoSemiColon)) {
    138                    throw new IllegalArgumentException("Semi-colon required at end of numeric entity");
    139                }
    140            }
    141
    142            final int entityValue;
    143            try {
    144                if (isHex) {
    145                    entityValue = Integer.parseInt(input.subSequence(start, end).toString(), 16);
    146                } else {
    147                    entityValue = Integer.parseInt(input.subSequence(start, end).toString(), 10);
    148                }
    149            } catch (final NumberFormatException nfe) {
    150                return 0;
    151            }
    152
    153            if (entityValue > 0xFFFF) {
    154                final char[] chars = Character.toChars(entityValue);
    155                out.write(chars[0]);
    156                out.write(chars[1]);
    157            } else {
    158                out.write(entityValue);
    159            }
    160
    161            return 2 + end - start + (isHex ? 1 : 0) + (semiNext ? 1 : 0);
    162        }
    163        return 0;
    164    }
    165}