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}