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}