    001// Copyright 2008, 2010 The Apache Software Foundation
    002//
    003// Licensed under the Apache License, Version 2.0 (the "License");
    004// you may not use this file except in compliance with the License.
    005// You may obtain a copy of the License at
    006//
    007// http://www.apache.org/licenses/LICENSE-2.0
    008//
    009// Unless required by applicable law or agreed to in writing, software
    010// distributed under the License is distributed on an "AS IS" BASIS,
    011// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    012// See the License for the specific language governing permissions and
    013// limitations under the License.
    014
    015package org.apache.tapestry5.internal.services;
    016
    017import java.util.BitSet;
    018
    019import org.apache.tapestry5.services.URLEncoder;
    020
    021public class URLEncoderImpl implements URLEncoder
    022{
    023    static final String ENCODED_NULL = "$N";
    024    static final String ENCODED_BLANK = "$B";
    025
    026    /**
    027     * Bit set indicating which character are safe to pass through (when encoding or decoding) as-is. All other
    028     * characters are encoded as a kind of unicode escape.
    029     */
    030    private final BitSet safe = new BitSet(128);
    031
    032    {
    033        markSafe("abcdefghijklmnopqrstuvwxyz");
    034        markSafe("ABCDEFGHIJKLMNOPQRSTUVWXYZ");
    035        markSafe("01234567890-_.:");
    036    }
    037
    038    private void markSafe(String s)
    039    {
    040        for (char ch : s.toCharArray())
    041        {
    042            safe.set((int) ch);
    043        }
    044    }
    045
    046    public String encode(String input)
    047    {
    048        if (input == null)
    049            return ENCODED_NULL;
    050
    051        if (input.equals(""))
    052            return ENCODED_BLANK;
    053
    054        boolean dirty = false;
    055
    056        int length = input.length();
    057
    058        StringBuilder output = new StringBuilder(length * 2);
    059
    060        for (int i = 0; i < length; i++)
    061        {
    062            char ch = input.charAt(i);
    063
    064            if (ch == '$')
    065            {
    066                output.append("$$");
    067                dirty = true;
    068                continue;
    069            }
    070
    071            int chAsInt = (int) ch;
    072
    073            if (safe.get(chAsInt))
    074            {
    075                output.append(ch);
    076                continue;
    077            }
    078
    079            output.append(String.format("$%04x", chAsInt));
    080            dirty = true;
    081        }
    082
    083        return dirty ? output.toString() : input;
    084    }
    085
    086    public String decode(String input)
    087    {
    088        assert input != null;
    089
    090        if (input.equals(ENCODED_NULL))
    091            return null;
    092
    093        if (input.equals(ENCODED_BLANK))
    094            return "";
    095
    096        boolean dirty = false;
    097
    098        int length = input.length();
    099
    100        StringBuilder output = new StringBuilder(length * 2);
    101
    102        for (int i = 0; i < length; i++)
    103        {
    104            char ch = input.charAt(i);
    105
    106            if (ch == '$')
    107            {
    108                dirty = true;
    109
    110                if (i + 1 < length && input.charAt(i + 1) == '$')
    111                {
    112                    output.append('$');
    113                    i++;
    114
    115                    dirty = true;
    116                    continue;
    117                }
    118
    119                if (i + 4 < length)
    120                {
    121                    String hex = input.substring(i + 1, i + 5);
    122
    123                    try
    124                    {
    125                        int unicode = Integer.parseInt(hex, 16);
    126
    127                        output.append((char) unicode);
    128                        i += 4;
    129                        dirty = true;
    130                        continue;
    131                    }
    132                    catch (NumberFormatException ex)
    133                    {
    134                        // Ignore.
    135                    }
    136                }
    137
    138                throw new IllegalArgumentException(
    139                        String.format(
    140                                "Input string '%s' is not valid; the '$' character at position %d should be followed by another '$' or a four digit hex number (a unicode value).",
    141                                input, i + 1));
    142            }
    143
    144            if (!safe.get((int) ch)) { throw new IllegalArgumentException(
    145                    String.format("Input string '%s' is not valid; the character '%s' at position %d is not valid.",
    146                            input, ch, i + 1)); }
    147
    148            output.append(ch);
    149        }
    150
    151        return dirty ? output.toString() : input;
    152    }
    153}