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
015 package org.apache.tapestry5.internal.services;
016
017 import java.util.BitSet;
018
019 import org.apache.tapestry5.services.URLEncoder;
020
021 public 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 }