001/******************************************************************************* 002The MIT License (MIT) 003 004Copyright (c) 2024 KILLCODING.COM 005 006Permission is hereby granted, free of charge, to any person obtaining a copy 007of this software and associated documentation files (the "Software"), to deal 008in the Software without restriction, including without limitation the rights 009to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 010copies of the Software, and to permit persons to whom the Software is 011furnished to do so, subject to the following conditions: 012 013The above copyright notice and this permission notice shall be included in 014all copies or substantial portions of the Software. 015 016THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 017IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 018FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 019AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 020LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 021OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 022THE SOFTWARE. 023*****************************************************************************/ 024package com.killcoding.tool; 025 026import java.io.IOException; 027import java.io.Reader; 028import java.io.Writer; 029 030 031public final class CodeEscape { 032 033 public static String escapeToSingleLineForCsv(final String text) { 034 035 if(text == null) return "null"; 036 037 return text.replaceAll(",","\\\\u002C").replaceAll("\n", "\\\\n").replaceAll("\r", "\\\\r").replaceAll("\t", "\\\\t"); 038 } 039 040 public static String escapeJavaMinimal(final String text) { 041 return escapeJava(text, CodeEscapeLevel.LEVEL_1_BASIC_ESCAPE_SET); 042 } 043 044 045 public static String escapeJava(final String text) { 046 return escapeJava(text, CodeEscapeLevel.LEVEL_2_ALL_NON_ASCII_PLUS_BASIC_ESCAPE_SET); 047 } 048 049 public static String escapeJava(final String text, final CodeEscapeLevel level) { 050 051 if (level == null) { 052 throw new IllegalArgumentException("The 'level' argument cannot be null"); 053 } 054 055 return CodeEscapeUtil.escape(text, level); 056 057 } 058 059 060 061 public static void escapeJavaMinimal(final String text, final Writer writer) 062 throws IOException { 063 escapeJava(text, writer, CodeEscapeLevel.LEVEL_1_BASIC_ESCAPE_SET); 064 } 065 066 067 public static void escapeJava(final String text, final Writer writer) 068 throws IOException { 069 escapeJava(text, writer, CodeEscapeLevel.LEVEL_2_ALL_NON_ASCII_PLUS_BASIC_ESCAPE_SET); 070 } 071 072 public static void escapeJava(final String text, final Writer writer, final CodeEscapeLevel level) 073 throws IOException { 074 075 if (writer == null) { 076 throw new IllegalArgumentException("Argument 'writer' cannot be null"); 077 } 078 079 if (level == null) { 080 throw new IllegalArgumentException("The 'level' argument cannot be null"); 081 } 082 083 CodeEscapeUtil.escape(new InternalStringReader(text), writer, level); 084 085 } 086 087 public static void escapeJavaMinimal(final Reader reader, final Writer writer) 088 throws IOException { 089 escapeJava(reader, writer, CodeEscapeLevel.LEVEL_1_BASIC_ESCAPE_SET); 090 } 091 092 public static void escapeJava(final Reader reader, final Writer writer) 093 throws IOException { 094 escapeJava(reader, writer, CodeEscapeLevel.LEVEL_2_ALL_NON_ASCII_PLUS_BASIC_ESCAPE_SET); 095 } 096 097 098 public static void escapeJava(final Reader reader, final Writer writer, final CodeEscapeLevel level) 099 throws IOException { 100 101 if (writer == null) { 102 throw new IllegalArgumentException("Argument 'writer' cannot be null"); 103 } 104 105 if (level == null) { 106 throw new IllegalArgumentException("The 'level' argument cannot be null"); 107 } 108 109 CodeEscapeUtil.escape(reader, writer, level); 110 111 } 112 113 public static void escapeJavaMinimal(final char[] text, final int offset, final int len, final Writer writer) 114 throws IOException { 115 escapeJava(text, offset, len, writer, CodeEscapeLevel.LEVEL_1_BASIC_ESCAPE_SET); 116 } 117 118 public static void escapeJava(final char[] text, final int offset, final int len, final Writer writer) 119 throws IOException { 120 escapeJava(text, offset, len, writer, CodeEscapeLevel.LEVEL_2_ALL_NON_ASCII_PLUS_BASIC_ESCAPE_SET); 121 } 122 123 public static void escapeJava(final char[] text, final int offset, final int len, final Writer writer, 124 final CodeEscapeLevel level) 125 throws IOException { 126 127 if (writer == null) { 128 throw new IllegalArgumentException("Argument 'writer' cannot be null"); 129 } 130 131 if (level == null) { 132 throw new IllegalArgumentException("The 'level' argument cannot be null"); 133 } 134 135 final int textLen = (text == null? 0 : text.length); 136 137 if (offset < 0 || offset > textLen) { 138 throw new IllegalArgumentException( 139 "Invalid (offset, len). offset=" + offset + ", len=" + len + ", text.length=" + textLen); 140 } 141 142 if (len < 0 || (offset + len) > textLen) { 143 throw new IllegalArgumentException( 144 "Invalid (offset, len). offset=" + offset + ", len=" + len + ", text.length=" + textLen); 145 } 146 147 CodeEscapeUtil.escape(text, offset, len, writer, level); 148 149 } 150 151 152 153 154 155 156 157 158 /** 159 * <p> 160 * Perform a Java <strong>unescape</strong> operation on a <tt>String</tt> input. 161 * </p> 162 * <p> 163 * No additional configuration arguments are required. Unescape operations 164 * will always perform <em>complete</em> Java unescape of SECs, u-based and octal escapes. 165 * </p> 166 * <p> 167 * This method is <strong>thread-safe</strong>. 168 * </p> 169 * 170 * @param text the <tt>String</tt> to be unescaped. 171 * @return The unescaped result <tt>String</tt>. As a memory-performance improvement, will return the exact 172 * same object as the <tt>text</tt> input argument if no unescaping modifications were required (and 173 * no additional <tt>String</tt> objects will be created during processing). Will 174 * return <tt>null</tt> if input is <tt>null</tt>. 175 */ 176 public static String unescapeJava(final String text) { 177 if (text == null) { 178 return null; 179 } 180 if (text.indexOf('\\') < 0) { 181 // Fail fast, avoid more complex (and less JIT-table) method to execute if not needed 182 return text; 183 } 184 return CodeEscapeUtil.unescape(text); 185 } 186 187 188 /** 189 * <p> 190 * Perform a Java <strong>unescape</strong> operation on a <tt>String</tt> input, writing results 191 * to a <tt>Writer</tt>. 192 * </p> 193 * <p> 194 * No additional configuration arguments are required. Unescape operations 195 * will always perform <em>complete</em> Java unescape of SECs, u-based and octal escapes. 196 * </p> 197 * <p> 198 * This method is <strong>thread-safe</strong>. 199 * </p> 200 * 201 * @param text the <tt>String</tt> to be unescaped. 202 * @param writer the <tt>java.io.Writer</tt> to which the unescaped result will be written. Nothing will 203 * be written at all to this writer if input is <tt>null</tt>. 204 * @throws IOException if an input/output exception occurs 205 * 206 * @since 1.1.2 207 */ 208 public static void unescapeJava(final String text, final Writer writer) 209 throws IOException { 210 211 if (writer == null) { 212 throw new IllegalArgumentException("Argument 'writer' cannot be null"); 213 } 214 if (text == null) { 215 return; 216 } 217 if (text.indexOf('\\') < 0) { 218 // Fail fast, avoid more complex (and less JIT-table) method to execute if not needed 219 writer.write(text); 220 return; 221 } 222 223 CodeEscapeUtil.unescape(new InternalStringReader(text), writer); 224 225 } 226 227 228 /** 229 * <p> 230 * Perform a Java <strong>unescape</strong> operation on a <tt>Reader</tt> input, writing results 231 * to a <tt>Writer</tt>. 232 * </p> 233 * <p> 234 * No additional configuration arguments are required. Unescape operations 235 * will always perform <em>complete</em> Java unescape of SECs, u-based and octal escapes. 236 * </p> 237 * <p> 238 * This method is <strong>thread-safe</strong>. 239 * </p> 240 * 241 * @param reader the <tt>Reader</tt> reading the text to be unescaped. 242 * @param writer the <tt>java.io.Writer</tt> to which the unescaped result will be written. Nothing will 243 * be written at all to this writer if input is <tt>null</tt>. 244 * @throws IOException if an input/output exception occurs 245 * 246 * @since 1.1.2 247 */ 248 public static void unescapeJava(final Reader reader, final Writer writer) 249 throws IOException { 250 251 if (writer == null) { 252 throw new IllegalArgumentException("Argument 'writer' cannot be null"); 253 } 254 255 CodeEscapeUtil.unescape(reader, writer); 256 257 } 258 259 260 /** 261 * <p> 262 * Perform a Java <strong>unescape</strong> operation on a <tt>char[]</tt> input. 263 * </p> 264 * <p> 265 * No additional configuration arguments are required. Unescape operations 266 * will always perform <em>complete</em> Java unescape of SECs, u-based and octal escapes. 267 * </p> 268 * <p> 269 * This method is <strong>thread-safe</strong>. 270 * </p> 271 * 272 * @param text the <tt>char[]</tt> to be unescaped. 273 * @param offset the position in <tt>text</tt> at which the unescape operation should start. 274 * @param len the number of characters in <tt>text</tt> that should be unescaped. 275 * @param writer the <tt>java.io.Writer</tt> to which the unescaped result will be written. Nothing will 276 * be written at all to this writer if input is <tt>null</tt>. 277 * @throws IOException if an input/output exception occurs 278 */ 279 public static void unescapeJava(final char[] text, final int offset, final int len, final Writer writer) 280 throws IOException{ 281 282 if (writer == null) { 283 throw new IllegalArgumentException("Argument 'writer' cannot be null"); 284 } 285 286 final int textLen = (text == null? 0 : text.length); 287 288 if (offset < 0 || offset > textLen) { 289 throw new IllegalArgumentException( 290 "Invalid (offset, len). offset=" + offset + ", len=" + len + ", text.length=" + textLen); 291 } 292 293 if (len < 0 || (offset + len) > textLen) { 294 throw new IllegalArgumentException( 295 "Invalid (offset, len). offset=" + offset + ", len=" + len + ", text.length=" + textLen); 296 } 297 298 CodeEscapeUtil.unescape(text, offset, len, writer); 299 300 } 301 302 303 304 305 private CodeEscape() { 306 super(); 307 } 308 309 310 311 /* 312 * This is basically a very simplified, thread-unsafe version of StringReader that should 313 * perform better than the original StringReader by removing all synchronization structures. 314 * 315 * Note the only implemented methods are those that we know are really used from within the 316 * stream-based escape/unescape operations. 317 */ 318 private static final class InternalStringReader extends Reader { 319 320 private String str; 321 private int length; 322 private int next = 0; 323 324 public InternalStringReader(final String s) { 325 super(); 326 this.str = s; 327 this.length = s.length(); 328 } 329 330 @Override 331 public int read() throws IOException { 332 if (this.next >= length) { 333 return -1; 334 } 335 return this.str.charAt(this.next++); 336 } 337 338 @Override 339 public int read(final char[] cbuf, final int off, final int len) throws IOException { 340 if ((off < 0) || (off > cbuf.length) || (len < 0) || 341 ((off + len) > cbuf.length) || ((off + len) < 0)) { 342 throw new IndexOutOfBoundsException(); 343 } else if (len == 0) { 344 return 0; 345 } 346 if (this.next >= this.length) { 347 return -1; 348 } 349 int n = Math.min(this.length - this.next, len); 350 this.str.getChars(this.next, this.next + n, cbuf, off); 351 this.next += n; 352 return n; 353 } 354 355 @Override 356 public void close() throws IOException { 357 this.str = null; // Just set the reference to null, help the GC 358 } 359 360 } 361 362 363}