001/* 002 * Copyright 2020 Anyware Services 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016package org.ametys.core.util; 017 018import java.io.ByteArrayOutputStream; 019import java.nio.charset.StandardCharsets; 020import java.util.Map; 021import java.util.function.Predicate; 022 023/** 024 * Utility class for encoding and decoding URL, following the RFC 3986 025 * @see <a href="https://tools.ietf.org/html/rfc3986">https://tools.ietf.org/html/rfc3986</a> 026 */ 027public final class URIUtils 028{ 029 private static final String __NAME_VALUE_SEPARATOR = "="; 030 private static final String __PARAMETER_SEPARATOR = "&"; 031 032 private static Predicate<Byte> _isAlpha = c -> c >= 'a' && c <= 'z' || c >= 'A' && c <= 'Z'; 033 private static Predicate<Byte> _isDigit = c -> c >= '0' && c <= '9'; 034 private static Predicate<Byte> _isSubDelimiter = c -> '!' == c || '$' == c || '&' == c || '\'' == c || '(' == c || ')' == c 035 || '*' == c || '+' == c || ',' == c || ';' == c || '=' == c; 036 private static Predicate<Byte> _isUnreserved = _isAlpha.or(_isDigit).or(c -> '-' == c || '.' == c || '_' == c || '~' == c); 037 private static Predicate<Byte> _isPchar = _isUnreserved.or(_isSubDelimiter).or(c -> ':' == c || '@' == c); 038 039 private URIUtils() 040 { 041 // empty 042 } 043 044 /** 045 * Encode a parameter's value using UTF-8 encoding 046 * @param value the value or the name of the request's parameter to encode 047 * @return the encoded value. 048 */ 049 public static String encodeParameter(String value) 050 { 051 return _encodeUriComponent(value, _isPchar.or(c -> '/' == c || '?' == c) 052 .and(Predicate.not(c -> '=' == c || '+' == c || '&' == c))); 053 } 054 055 /** 056 * Encode an URL path 057 * @param path the path to encode (before question-mark character) 058 * @return the encoded path. 059 */ 060 public static String encodePath(String path) 061 { 062 return _encodeUriComponent(path, _isPchar.or(c -> '/' == c) 063 .and(Predicate.not(c -> ';' == c))); 064 } 065 066 /** 067 * Encode an URL path segment 068 * @param pathSegment the path segment to encode 069 * @return the encoded path segment. 070 */ 071 public static String encodePathSegment(String pathSegment) 072 { 073 return _encodeUriComponent(pathSegment, _isPchar.and(Predicate.not(c -> ';' == c))); 074 } 075 076 /** 077 * Encode a request header value. 078 * @param header the value to encode 079 * @return the encoded value. 080 */ 081 public static String encodeHeader(String header) 082 { 083 return _encodeUriComponent(header, _isUnreserved); 084 } 085 086 // Implementation taken from Spring's UriUtils#encode 087 @SuppressWarnings("all") 088 private static String _encodeUriComponent(String uriComponent, Predicate<Byte> charactersToKeep) 089 { 090 byte[] bytes = uriComponent.getBytes(StandardCharsets.UTF_8); 091 ByteArrayOutputStream bos = new ByteArrayOutputStream(bytes.length); 092 093 for (byte b : bytes) 094 { 095 if (b < 0) 096 { 097 b += 256; 098 } 099 if (charactersToKeep.test(b)) 100 { 101 bos.write(b); 102 } 103 else 104 { 105 bos.write('%'); 106 char hex1 = Character.toUpperCase(Character.forDigit((b >> 4) & 0xF, 16)); 107 char hex2 = Character.toUpperCase(Character.forDigit(b & 0xF, 16)); 108 bos.write(hex1); 109 bos.write(hex2); 110 } 111 } 112 113 return new String(bos.toByteArray(), StandardCharsets.US_ASCII); 114 } 115 116 /** 117 * Encode an URL path 118 * @param path the path to encode (before question-mark character) 119 * @param parameters the parameters. Can be null. 120 * @return the encoded path 121 */ 122 public static String encodeURI(String path, Map<String, String> parameters) 123 { 124 return _buildURI(path, parameters, true); 125 } 126 127 /** 128 * Build an URL. 129 * @param path the URL path. 130 * @param parameters the URL parameters. Can be null. 131 * @return the computed URL. 132 */ 133 public static String buildURI(String path, Map<String, String> parameters) 134 { 135 return _buildURI(path, parameters, false); 136 } 137 138 private static String _buildURI(String path, Map<String, String> parameters, boolean encode) 139 { 140 StringBuilder sb = new StringBuilder(); 141 sb.append(encode ? encodePath(path) : path); 142 143 if (parameters != null && !parameters.isEmpty()) 144 { 145 StringBuilder query = new StringBuilder(); 146 147 for (String paramName : parameters.keySet()) 148 { 149 String encodedName = encode ? encodeParameter(paramName) : paramName; 150 String encodedValue = encode ? encodeParameter(parameters.get(paramName)) : parameters.get(paramName); 151 152 if (query.length() > 0) 153 { 154 query.append(__PARAMETER_SEPARATOR); 155 } 156 157 query.append(encodedName) 158 .append(__NAME_VALUE_SEPARATOR) 159 .append(encodedValue); 160 } 161 162 sb.append("?").append(query.toString()); 163 } 164 165 return sb.toString(); 166 } 167 168 /** 169 * Decodes an URI-encoded String. 170 * @param source the String to decode. 171 * @return the decoded String. 172 */ 173 // Implementation taken from Spring's UriUtils#decode 174 @SuppressWarnings("all") 175 public static String decode(String source) 176 { 177 int length = source.length(); 178 ByteArrayOutputStream bos = new ByteArrayOutputStream(length); 179 boolean changed = false; 180 for (int i = 0; i < length; i++) 181 { 182 char ch = source.charAt(i); 183 184 if (ch == '%') 185 { 186 if ((i + 2) < length) 187 { 188 char hex1 = source.charAt(i + 1); 189 char hex2 = source.charAt(i + 2); 190 int u = Character.digit(hex1, 16); 191 int l = Character.digit(hex2, 16); 192 193 if (u == -1 || l == -1) 194 { 195 throw new IllegalArgumentException("Invalid encoded sequence \"" + source.substring(i) + "\""); 196 } 197 198 bos.write((byte) ((u << 4) + l)); 199 200 i += 2; 201 changed = true; 202 } 203 else 204 { 205 throw new IllegalArgumentException("Invalid encoded sequence \"" + source.substring(i) + "\""); 206 } 207 } 208 else if (ch > 0 && ch < 128) 209 { 210 // to avoid the overhead of decoding/recoding an ASCII char 211 bos.write(ch); 212 } 213 else if (Character.isHighSurrogate(ch)) 214 { 215 if ((i + 1) < length && Character.isLowSurrogate(source.charAt(i + 1))) 216 { 217 for (byte b : String.valueOf(new char[]{ch, source.charAt(i + 1)}).getBytes(StandardCharsets.UTF_8)) 218 { 219 bos.write(b); 220 } 221 222 i += 1; 223 } 224 else 225 { 226 throw new IllegalArgumentException("Invalid encoded sequence \"" + source.substring(i) + "\""); 227 } 228 } 229 else 230 { 231 for (byte b : String.valueOf(ch).getBytes(StandardCharsets.UTF_8)) 232 { 233 bos.write(b); 234 } 235 } 236 } 237 238 return changed ? new String(bos.toByteArray(), StandardCharsets.UTF_8) : source; 239 } 240}