Move restconf.common.util
[netconf.git] / restconf / restconf-nb-rfc8040 / src / main / java / org / opendaylight / restconf / nb / rfc8040 / Utf8Buffer.java
1 /*
2  * Copyright (c) 2021 PANTHEON.tech, s.r.o. and others.  All rights reserved.
3  *
4  * This program and the accompanying materials are made available under the
5  * terms of the Eclipse Public License v1.0 which accompanies this distribution,
6  * and is available at http://www.eclipse.org/legal/epl-v10.html
7  */
8 package org.opendaylight.restconf.nb.rfc8040;
9
10 import java.io.ByteArrayOutputStream;
11 import java.nio.ByteBuffer;
12 import java.nio.charset.CharacterCodingException;
13 import java.nio.charset.CharsetDecoder;
14 import java.nio.charset.CodingErrorAction;
15 import java.nio.charset.StandardCharsets;
16 import java.text.ParseException;
17 import org.eclipse.jdt.annotation.NonNull;
18 import org.opendaylight.odlparent.logging.markers.Markers;
19 import org.opendaylight.yangtools.concepts.Mutable;
20 import org.slf4j.Logger;
21 import org.slf4j.LoggerFactory;
22
23 /**
24  * A buffer of bytes in lazily-allocated array, which can be appended with {@link #appendByte(int)}. Current contents
25  * can be transferred to a {@link StringBuilder} via {@link #flushTo(StringBuilder, int)}, which performs UTF-8
26  * character decoding.
27  */
28 final class Utf8Buffer implements Mutable {
29     private static final Logger LOG = LoggerFactory.getLogger(Utf8Buffer.class);
30
31     private ByteArrayOutputStream bos;
32     private CharsetDecoder decoder;
33
34     void appendByte(final byte value) {
35         var buf = bos;
36         if (buf == null) {
37             bos = buf = new ByteArrayOutputStream(8);
38         }
39         buf.write(value);
40     }
41
42     void flushTo(final @NonNull StringBuilder sb, final int errorOffset) throws ParseException {
43         final var buf = bos;
44         if (buf != null && buf.size() != 0) {
45             flushTo(sb, buf, errorOffset);
46         }
47     }
48
49     // Split out to aid inlining
50     private void flushTo(final StringBuilder sb, final ByteArrayOutputStream buf, final int errorOffset)
51             throws ParseException {
52         final var bytes = buf.toByteArray();
53         buf.reset();
54
55         // Special case for a single ASCII character, side-steps decoder/bytebuf allocation
56         if (bytes.length == 1) {
57             final byte ch = bytes[0];
58             if (ch >= 0) {
59                 sb.append((char) ch);
60                 return;
61             }
62         }
63         try {
64             append(sb, ByteBuffer.wrap(bytes));
65         } catch (CharacterCodingException e) {
66             throw report(errorOffset, bytes, e);
67         }
68     }
69
70     private void append(final StringBuilder sb, final ByteBuffer bytes) throws CharacterCodingException {
71         var local = decoder;
72         if (local == null) {
73             decoder = local = StandardCharsets.UTF_8.newDecoder()
74                 .onMalformedInput(CodingErrorAction.REPORT)
75                 .onUnmappableCharacter(CodingErrorAction.REPORT);
76         }
77         sb.append(local.decode(bytes));
78     }
79
80     // Split out to silence checkstyle's failure to understand we cannot propagate the cause
81     private static ParseException report(final int errorOffset, final byte[] bytes,
82             final CharacterCodingException cause) {
83         final String str = new String(bytes, StandardCharsets.UTF_8);
84         LOG.debug(Markers.confidential(), "Rejecting invalid UTF-8 sequence '{}'", str, cause);
85         return new ParseException("Invalid UTF-8 sequence '" + str + "': " + cause.getMessage(), errorOffset);
86     }
87 }