80450e5fe940e9d98f2f013d5e41740a898e853b
[yangtools.git] / codec / yang-data-codec-binfmt / src / main / java / org / opendaylight / yangtools / yang / data / codec / binfmt / MagnesiumDataOutput.java
1 /*
2  * Copyright (c) 2019 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.yangtools.yang.data.codec.binfmt;
9
10 import static com.google.common.base.Preconditions.checkArgument;
11
12 import java.io.DataOutput;
13 import java.io.IOException;
14 import java.io.StringWriter;
15 import java.math.BigDecimal;
16 import java.nio.charset.StandardCharsets;
17 import java.util.ArrayDeque;
18 import java.util.Deque;
19 import java.util.HashMap;
20 import java.util.List;
21 import java.util.Map;
22 import java.util.Map.Entry;
23 import java.util.Optional;
24 import java.util.Set;
25 import javax.xml.transform.TransformerException;
26 import javax.xml.transform.TransformerFactory;
27 import javax.xml.transform.dom.DOMSource;
28 import javax.xml.transform.stream.StreamResult;
29 import org.eclipse.jdt.annotation.NonNull;
30 import org.opendaylight.yangtools.yang.common.Decimal64;
31 import org.opendaylight.yangtools.yang.common.Empty;
32 import org.opendaylight.yangtools.yang.common.QName;
33 import org.opendaylight.yangtools.yang.common.QNameModule;
34 import org.opendaylight.yangtools.yang.common.Revision;
35 import org.opendaylight.yangtools.yang.common.Uint16;
36 import org.opendaylight.yangtools.yang.common.Uint32;
37 import org.opendaylight.yangtools.yang.common.Uint64;
38 import org.opendaylight.yangtools.yang.common.Uint8;
39 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier;
40 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.AugmentationIdentifier;
41 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeIdentifier;
42 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeIdentifierWithPredicates;
43 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeWithValue;
44 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.PathArgument;
45 import org.slf4j.Logger;
46 import org.slf4j.LoggerFactory;
47
48 /**
49  * Abstract base class for NormalizedNodeDataOutput based on {@link MagnesiumNode}, {@link MagnesiumPathArgument} and
50  * {@link MagnesiumValue}.
51  */
52 final class MagnesiumDataOutput extends AbstractNormalizedNodeDataOutput {
53     private static final Logger LOG = LoggerFactory.getLogger(MagnesiumDataOutput.class);
54
55     // Marker for encoding state when we have entered startLeafNode() within a startMapEntry() and that leaf corresponds
56     // to a key carried within NodeIdentifierWithPredicates.
57     private static final Object KEY_LEAF_STATE = new Object();
58     // Marker for nodes which have simple content and do not use END_NODE marker to terminate
59     private static final Object NO_ENDNODE_STATE = new Object();
60
61     private static final TransformerFactory TF = TransformerFactory.newInstance();
62
63     /**
64      * Stack tracking encoding state. In general we track the node identifier of the currently-open element, but there
65      * are a few other circumstances where we push other objects. See {@link #KEY_LEAF_STATE} and
66      * {@link #NO_ENDNODE_STATE}.
67      */
68     private final Deque<Object> stack = new ArrayDeque<>();
69
70     // Coding maps
71     private final Map<AugmentationIdentifier, Integer> aidCodeMap = new HashMap<>();
72     private final Map<QNameModule, Integer> moduleCodeMap = new HashMap<>();
73     private final Map<String, Integer> stringCodeMap = new HashMap<>();
74     private final Map<QName, Integer> qnameCodeMap = new HashMap<>();
75
76     MagnesiumDataOutput(final DataOutput output) {
77         super(output);
78     }
79
80     @Override
81     public void startLeafNode(final NodeIdentifier name) throws IOException {
82         final Object current = stack.peek();
83         if (current instanceof NodeIdentifierWithPredicates nip) {
84             final QName qname = name.getNodeType();
85             if (nip.containsKey(qname)) {
86                 writeQNameNode(MagnesiumNode.NODE_LEAF | MagnesiumNode.PREDICATE_ONE, qname);
87                 stack.push(KEY_LEAF_STATE);
88                 return;
89             }
90         }
91
92         startSimpleNode(MagnesiumNode.NODE_LEAF, name);
93     }
94
95     @Override
96     public void startLeafSet(final NodeIdentifier name, final int childSizeHint) throws IOException {
97         startQNameNode(MagnesiumNode.NODE_LEAFSET, name);
98     }
99
100     @Override
101     public void startOrderedLeafSet(final NodeIdentifier name, final int childSizeHint) throws IOException {
102         startQNameNode(MagnesiumNode.NODE_LEAFSET_ORDERED, name);
103     }
104
105     @Override
106     public void startLeafSetEntryNode(final NodeWithValue<?> name) throws IOException {
107         if (matchesParentQName(name.getNodeType())) {
108             output.writeByte(MagnesiumNode.NODE_LEAFSET_ENTRY);
109             stack.push(NO_ENDNODE_STATE);
110         } else {
111             startSimpleNode(MagnesiumNode.NODE_LEAFSET_ENTRY, name);
112         }
113     }
114
115     @Override
116     public void startContainerNode(final NodeIdentifier name, final int childSizeHint) throws IOException {
117         startQNameNode(MagnesiumNode.NODE_CONTAINER, name);
118     }
119
120     @Override
121     public void startUnkeyedList(final NodeIdentifier name, final int childSizeHint) throws IOException {
122         startQNameNode(MagnesiumNode.NODE_LIST, name);
123     }
124
125     @Override
126     public void startUnkeyedListItem(final NodeIdentifier name, final int childSizeHint) throws IOException {
127         startInheritedNode(MagnesiumNode.NODE_LIST_ENTRY, name);
128     }
129
130     @Override
131     public void startMapNode(final NodeIdentifier name, final int childSizeHint) throws IOException {
132         startQNameNode(MagnesiumNode.NODE_MAP, name);
133     }
134
135     @Override
136     public void startMapEntryNode(final NodeIdentifierWithPredicates identifier, final int childSizeHint)
137             throws IOException {
138         final int size = identifier.size();
139         if (size == 1) {
140             startInheritedNode((byte) (MagnesiumNode.NODE_MAP_ENTRY | MagnesiumNode.PREDICATE_ONE), identifier);
141         } else if (size == 0) {
142             startInheritedNode((byte) (MagnesiumNode.NODE_MAP_ENTRY | MagnesiumNode.PREDICATE_ZERO), identifier);
143         } else if (size < 256) {
144             startInheritedNode((byte) (MagnesiumNode.NODE_MAP_ENTRY | MagnesiumNode.PREDICATE_1B), identifier);
145             output.writeByte(size);
146         } else {
147             startInheritedNode((byte) (MagnesiumNode.NODE_MAP_ENTRY | MagnesiumNode.PREDICATE_4B), identifier);
148             output.writeInt(size);
149         }
150
151         writePredicates(identifier);
152     }
153
154     @Override
155     public void startOrderedMapNode(final NodeIdentifier name, final int childSizeHint) throws IOException {
156         startQNameNode(MagnesiumNode.NODE_MAP_ORDERED, name);
157     }
158
159     @Override
160     public void startChoiceNode(final NodeIdentifier name, final int childSizeHint) throws IOException {
161         startQNameNode(MagnesiumNode.NODE_CHOICE, name);
162     }
163
164     @Override
165     public void startAugmentationNode(final AugmentationIdentifier identifier) throws IOException {
166         final Integer code = aidCodeMap.get(identifier);
167         if (code == null) {
168             aidCodeMap.put(identifier, aidCodeMap.size());
169             output.writeByte(MagnesiumNode.NODE_AUGMENTATION | MagnesiumNode.ADDR_DEFINE);
170             final Set<QName> qnames = identifier.getPossibleChildNames();
171             output.writeInt(qnames.size());
172             for (QName qname : qnames) {
173                 writeQNameInternal(qname);
174             }
175         } else {
176             writeNodeType(MagnesiumNode.NODE_AUGMENTATION, code);
177         }
178         stack.push(identifier);
179     }
180
181     @Override
182     public boolean startAnyxmlNode(final NodeIdentifier name, final Class<?> objectModel) throws IOException {
183         if (DOMSource.class.isAssignableFrom(objectModel)) {
184             startSimpleNode(MagnesiumNode.NODE_ANYXML, name);
185             return true;
186         }
187         return false;
188     }
189
190     @Override
191     public void domSourceValue(final DOMSource value) throws IOException {
192         final StringWriter writer = new StringWriter();
193         try {
194             TF.newTransformer().transform(value, new StreamResult(writer));
195         } catch (TransformerException e) {
196             throw new IOException("Error writing anyXml", e);
197         }
198         writeValue(writer.toString());
199     }
200
201     @Override
202     public void endNode() throws IOException {
203         if (stack.pop() instanceof PathArgument) {
204             output.writeByte(MagnesiumNode.NODE_END);
205         }
206     }
207
208     @Override
209     public void scalarValue(final Object value) throws IOException {
210         if (KEY_LEAF_STATE.equals(stack.peek())) {
211             LOG.trace("Inside a map entry key leaf, not emitting value {}", value);
212         } else {
213             writeObject(value);
214         }
215     }
216
217     @Override short streamVersion() {
218         return TokenTypes.MAGNESIUM_VERSION;
219     }
220
221     @Override void writeQNameInternal(final QName qname) throws IOException {
222         final Integer code = qnameCodeMap.get(qname);
223         if (code == null) {
224             output.writeByte(MagnesiumValue.QNAME);
225             encodeQName(qname);
226         } else {
227             writeQNameRef(code);
228         }
229     }
230
231     @Override void writePathArgumentInternal(final PathArgument pathArgument) throws IOException {
232         if (pathArgument instanceof NodeIdentifier nid) {
233             writeNodeIdentifier(nid);
234         } else if (pathArgument instanceof NodeIdentifierWithPredicates nip) {
235             writeNodeIdentifierWithPredicates(nip);
236         } else if (pathArgument instanceof AugmentationIdentifier augid) {
237             writeAugmentationIdentifier(augid);
238         } else if (pathArgument instanceof NodeWithValue<?> niv) {
239             writeNodeWithValue(niv);
240         } else {
241             throw new IOException("Unhandled PathArgument " + pathArgument);
242         }
243     }
244
245     private void writeAugmentationIdentifier(final AugmentationIdentifier identifier) throws IOException {
246         final Set<QName> qnames = identifier.getPossibleChildNames();
247         final int size = qnames.size();
248         if (size < 29) {
249             output.writeByte(MagnesiumPathArgument.AUGMENTATION_IDENTIFIER
250                 | size << MagnesiumPathArgument.AID_COUNT_SHIFT);
251         } else if (size < 256) {
252             output.writeByte(MagnesiumPathArgument.AUGMENTATION_IDENTIFIER | MagnesiumPathArgument.AID_COUNT_1B);
253             output.writeByte(size);
254         } else if (size < 65536) {
255             output.writeByte(MagnesiumPathArgument.AUGMENTATION_IDENTIFIER | MagnesiumPathArgument.AID_COUNT_2B);
256             output.writeShort(size);
257         } else {
258             output.writeByte(MagnesiumPathArgument.AUGMENTATION_IDENTIFIER | MagnesiumPathArgument.AID_COUNT_4B);
259             output.writeInt(size);
260         }
261
262         for (QName qname : qnames) {
263             writeQNameInternal(qname);
264         }
265     }
266
267     private void writeNodeIdentifier(final NodeIdentifier identifier) throws IOException {
268         writePathArgumentQName(identifier.getNodeType(), MagnesiumPathArgument.NODE_IDENTIFIER);
269     }
270
271     private void writeNodeIdentifierWithPredicates(final NodeIdentifierWithPredicates identifier) throws IOException {
272         final int size = identifier.size();
273         if (size < 5) {
274             writePathArgumentQName(identifier.getNodeType(),
275                 (byte) (MagnesiumPathArgument.NODE_IDENTIFIER_WITH_PREDICATES
276                         | size << MagnesiumPathArgument.SIZE_SHIFT));
277         } else if (size < 256) {
278             writePathArgumentQName(identifier.getNodeType(),
279                 (byte) (MagnesiumPathArgument.NODE_IDENTIFIER_WITH_PREDICATES | MagnesiumPathArgument.SIZE_1B));
280             output.writeByte(size);
281         } else if (size < 65536) {
282             writePathArgumentQName(identifier.getNodeType(),
283                 (byte) (MagnesiumPathArgument.NODE_IDENTIFIER_WITH_PREDICATES | MagnesiumPathArgument.SIZE_2B));
284             output.writeShort(size);
285         } else {
286             writePathArgumentQName(identifier.getNodeType(),
287                 (byte) (MagnesiumPathArgument.NODE_IDENTIFIER_WITH_PREDICATES | MagnesiumPathArgument.SIZE_4B));
288             output.writeInt(size);
289         }
290
291         writePredicates(identifier);
292     }
293
294     private void writePredicates(final NodeIdentifierWithPredicates identifier) throws IOException {
295         for (Entry<QName, Object> e : identifier.entrySet()) {
296             writeQNameInternal(e.getKey());
297             writeObject(e.getValue());
298         }
299     }
300
301     private void writeNodeWithValue(final NodeWithValue<?> identifier) throws IOException {
302         writePathArgumentQName(identifier.getNodeType(), MagnesiumPathArgument.NODE_WITH_VALUE);
303         writeObject(identifier.getValue());
304     }
305
306     private void writePathArgumentQName(final QName qname, final byte typeHeader) throws IOException {
307         final Integer code = qnameCodeMap.get(qname);
308         if (code != null) {
309             final int val = code;
310             if (val < 256) {
311                 output.writeByte(typeHeader | MagnesiumPathArgument.QNAME_REF_1B);
312                 output.writeByte(val);
313             } else if (val < 65792) {
314                 output.writeByte(typeHeader | MagnesiumPathArgument.QNAME_REF_2B);
315                 output.writeShort(val - 256);
316             } else {
317                 output.writeByte(typeHeader | MagnesiumPathArgument.QNAME_REF_4B);
318                 output.writeInt(val);
319             }
320         } else {
321             // implied '| MagnesiumPathArgument.QNAME_DEF'
322             output.writeByte(typeHeader);
323             encodeQName(qname);
324         }
325     }
326
327     @Override void writeYangInstanceIdentifierInternal(final YangInstanceIdentifier identifier) throws IOException {
328         writeValue(identifier);
329     }
330
331     private void writeObject(final @NonNull Object value) throws IOException {
332         if (value instanceof String str) {
333             writeValue(str);
334         } else if (value instanceof Boolean bool) {
335             writeValue(bool);
336         } else if (value instanceof Byte byteVal) {
337             writeValue(byteVal);
338         } else if (value instanceof Short shortVal) {
339             writeValue(shortVal);
340         } else if (value instanceof Integer intVal) {
341             writeValue(intVal);
342         } else if (value instanceof Long longVal) {
343             writeValue(longVal);
344         } else if (value instanceof Uint8 uint8) {
345             writeValue(uint8);
346         } else if (value instanceof Uint16 uint16) {
347             writeValue(uint16);
348         } else if (value instanceof Uint32 uint32) {
349             writeValue(uint32);
350         } else if (value instanceof Uint64 uint64) {
351             writeValue(uint64);
352         } else if (value instanceof QName qname) {
353             writeQNameInternal(qname);
354         } else if (value instanceof YangInstanceIdentifier id) {
355             writeValue(id);
356         } else if (value instanceof byte[] bytes) {
357             writeValue(bytes);
358         } else if (value instanceof Empty) {
359             output.writeByte(MagnesiumValue.EMPTY);
360         } else if (value instanceof Set<?> set) {
361             writeValue(set);
362         } else if (value instanceof BigDecimal || value instanceof Decimal64) {
363             output.writeByte(MagnesiumValue.BIGDECIMAL);
364             output.writeUTF(value.toString());
365         } else {
366             throw new IOException("Unhandled value type " + value.getClass());
367         }
368     }
369
370     private void writeValue(final boolean value) throws IOException {
371         output.writeByte(value ? MagnesiumValue.BOOLEAN_TRUE : MagnesiumValue.BOOLEAN_FALSE);
372     }
373
374     private void writeValue(final byte value) throws IOException {
375         if (value != 0) {
376             output.writeByte(MagnesiumValue.INT8);
377             output.writeByte(value);
378         } else {
379             output.writeByte(MagnesiumValue.INT8_0);
380         }
381     }
382
383     private void writeValue(final short value) throws IOException {
384         if (value != 0) {
385             output.writeByte(MagnesiumValue.INT16);
386             output.writeShort(value);
387         } else {
388             output.writeByte(MagnesiumValue.INT16_0);
389         }
390     }
391
392     private void writeValue(final int value) throws IOException {
393         if ((value & 0xFFFF0000) != 0) {
394             output.writeByte(MagnesiumValue.INT32);
395             output.writeInt(value);
396         } else if (value != 0) {
397             output.writeByte(MagnesiumValue.INT32_2B);
398             output.writeShort(value);
399         } else {
400             output.writeByte(MagnesiumValue.INT32_0);
401         }
402     }
403
404     private void writeValue(final long value) throws IOException {
405         if ((value & 0xFFFFFFFF00000000L) != 0) {
406             output.writeByte(MagnesiumValue.INT64);
407             output.writeLong(value);
408         } else if (value != 0) {
409             output.writeByte(MagnesiumValue.INT64_4B);
410             output.writeInt((int) value);
411         } else {
412             output.writeByte(MagnesiumValue.INT64_0);
413         }
414     }
415
416     private void writeValue(final Uint8 value) throws IOException {
417         final byte b = value.byteValue();
418         if (b != 0) {
419             output.writeByte(MagnesiumValue.UINT8);
420             output.writeByte(b);
421         } else {
422             output.writeByte(MagnesiumValue.UINT8_0);
423         }
424     }
425
426     private void writeValue(final Uint16 value) throws IOException {
427         final short s = value.shortValue();
428         if (s != 0) {
429             output.writeByte(MagnesiumValue.UINT16);
430             output.writeShort(s);
431         } else {
432             output.writeByte(MagnesiumValue.UINT16_0);
433         }
434     }
435
436     private void writeValue(final Uint32 value) throws IOException {
437         final int i = value.intValue();
438         if ((i & 0xFFFF0000) != 0) {
439             output.writeByte(MagnesiumValue.UINT32);
440             output.writeInt(i);
441         } else if (i != 0) {
442             output.writeByte(MagnesiumValue.UINT32_2B);
443             output.writeShort(i);
444         } else {
445             output.writeByte(MagnesiumValue.UINT32_0);
446         }
447     }
448
449     private void writeValue(final Uint64 value) throws IOException {
450         final long l = value.longValue();
451         if ((l & 0xFFFFFFFF00000000L) != 0) {
452             output.writeByte(MagnesiumValue.UINT64);
453             output.writeLong(l);
454         } else if (l != 0) {
455             output.writeByte(MagnesiumValue.UINT64_4B);
456             output.writeInt((int) l);
457         } else {
458             output.writeByte(MagnesiumValue.UINT64_0);
459         }
460     }
461
462     private void writeValue(final String value) throws IOException {
463         if (value.isEmpty()) {
464             output.writeByte(MagnesiumValue.STRING_EMPTY);
465         } else if (value.length() <= Short.MAX_VALUE / 2) {
466             output.writeByte(MagnesiumValue.STRING_UTF);
467             output.writeUTF(value);
468         } else if (value.length() <= 1048576) {
469             final byte[] bytes = value.getBytes(StandardCharsets.UTF_8);
470             if (bytes.length < 65536) {
471                 output.writeByte(MagnesiumValue.STRING_2B);
472                 output.writeShort(bytes.length);
473             } else {
474                 output.writeByte(MagnesiumValue.STRING_4B);
475                 output.writeInt(bytes.length);
476             }
477             output.write(bytes);
478         } else {
479             output.writeByte(MagnesiumValue.STRING_CHARS);
480             output.writeInt(value.length());
481             output.writeChars(value);
482         }
483     }
484
485     private void writeValue(final byte[] value) throws IOException {
486         if (value.length < 128) {
487             output.writeByte(MagnesiumValue.BINARY_0 + value.length);
488         } else if (value.length < 384) {
489             output.writeByte(MagnesiumValue.BINARY_1B);
490             output.writeByte(value.length - 128);
491         } else if (value.length < 65920) {
492             output.writeByte(MagnesiumValue.BINARY_2B);
493             output.writeShort(value.length - 384);
494         } else {
495             output.writeByte(MagnesiumValue.BINARY_4B);
496             output.writeInt(value.length);
497         }
498         output.write(value);
499     }
500
501     private void writeValue(final YangInstanceIdentifier value) throws IOException {
502         final List<PathArgument> args = value.getPathArguments();
503         final int size = args.size();
504         if (size > 31) {
505             output.writeByte(MagnesiumValue.YIID);
506             output.writeInt(size);
507         } else {
508             output.writeByte(MagnesiumValue.YIID_0 + size);
509         }
510         for (PathArgument arg : args) {
511             writePathArgumentInternal(arg);
512         }
513     }
514
515     private void writeValue(final Set<?> value) throws IOException {
516         final int size = value.size();
517         if (size < 29) {
518             output.writeByte(MagnesiumValue.BITS_0 + size);
519         } else if (size < 285) {
520             output.writeByte(MagnesiumValue.BITS_1B);
521             output.writeByte(size - 29);
522         } else if (size < 65821) {
523             output.writeByte(MagnesiumValue.BITS_2B);
524             output.writeShort(size - 285);
525         } else {
526             output.writeByte(MagnesiumValue.BITS_4B);
527             output.writeInt(size);
528         }
529
530         for (Object bit : value) {
531             checkArgument(bit instanceof String, "Expected value type to be String but was %s", bit);
532             encodeString((String) bit);
533         }
534     }
535
536     // Check if the proposed QName matches the parent. This is only effective if the parent is identified by
537     // NodeIdentifier -- which is typically true
538     private boolean matchesParentQName(final QName qname) {
539         final Object current = stack.peek();
540         return current instanceof NodeIdentifier nid && qname.equals(nid.getNodeType());
541     }
542
543     // Start an END_NODE-terminated node, which typically has a QName matching the parent. If that is the case we emit
544     // a parent reference instead of an explicit QName reference -- saving at least one byte
545     private void startInheritedNode(final byte type, final PathArgument name) throws IOException {
546         final QName qname = name.getNodeType();
547         if (matchesParentQName(qname)) {
548             output.write(type);
549         } else {
550             writeQNameNode(type, qname);
551         }
552         stack.push(name);
553     }
554
555     // Start an END_NODE-terminated node, which needs its QName encoded
556     private void startQNameNode(final byte type, final PathArgument name) throws IOException {
557         writeQNameNode(type, name.getNodeType());
558         stack.push(name);
559     }
560
561     // Start a simple node, which is not terminated through END_NODE and encode its QName
562     private void startSimpleNode(final byte type, final PathArgument name) throws IOException {
563         writeQNameNode(type, name.getNodeType());
564         stack.push(NO_ENDNODE_STATE);
565     }
566
567     // Encode a QName-based (i.e. NodeIdentifier*) node with a particular QName. This will either result in a QName
568     // definition, or a reference, where this is encoded along with the node type.
569     private void writeQNameNode(final int type, final @NonNull QName qname) throws IOException {
570         final Integer code = qnameCodeMap.get(qname);
571         if (code == null) {
572             output.writeByte(type | MagnesiumNode.ADDR_DEFINE);
573             encodeQName(qname);
574         } else {
575             writeNodeType(type, code);
576         }
577     }
578
579     // Write a node type + lookup
580     private void writeNodeType(final int type, final int code) throws IOException {
581         if (code <= 255) {
582             output.writeByte(type | MagnesiumNode.ADDR_LOOKUP_1B);
583             output.writeByte(code);
584         } else {
585             output.writeByte(type | MagnesiumNode.ADDR_LOOKUP_4B);
586             output.writeInt(code);
587         }
588     }
589
590     // Encode a QName using lookup tables, resuling either in a reference to an existing entry, or emitting two
591     // String values.
592     private void encodeQName(final @NonNull QName qname) throws IOException {
593         final Integer prev = qnameCodeMap.put(qname, qnameCodeMap.size());
594         if (prev != null) {
595             throw new IOException("Internal coding error: attempted to re-encode " + qname + "%s already encoded as "
596                     + prev);
597         }
598
599         final QNameModule module = qname.getModule();
600         final Integer code = moduleCodeMap.get(module);
601         if (code == null) {
602             moduleCodeMap.put(module, moduleCodeMap.size());
603             encodeString(module.getNamespace().toString());
604             final Optional<Revision> rev = module.getRevision();
605             if (rev.isPresent()) {
606                 encodeString(rev.orElseThrow().toString());
607             } else {
608                 output.writeByte(MagnesiumValue.STRING_EMPTY);
609             }
610         } else {
611             writeModuleRef(code);
612         }
613         encodeString(qname.getLocalName());
614     }
615
616     // Encode a String using lookup tables, resulting either in a reference to an existing entry, or emitting as
617     // a literal value
618     private void encodeString(final @NonNull String str) throws IOException {
619         final Integer code = stringCodeMap.get(str);
620         if (code != null) {
621             writeRef(code);
622         } else {
623             stringCodeMap.put(str, stringCodeMap.size());
624             writeValue(str);
625         }
626     }
627
628     // Write a QName with a lookup table reference. This is a combination of asserting the value is a QName plus
629     // the effects of writeRef()
630     private void writeQNameRef(final int code) throws IOException {
631         final int val = code;
632         if (val < 256) {
633             output.writeByte(MagnesiumValue.QNAME_REF_1B);
634             output.writeByte(val);
635         } else if (val < 65792) {
636             output.writeByte(MagnesiumValue.QNAME_REF_2B);
637             output.writeShort(val - 256);
638         } else {
639             output.writeByte(MagnesiumValue.QNAME_REF_4B);
640             output.writeInt(val);
641         }
642     }
643
644     // Write a lookup table reference, which table is being referenced is implied by the caller
645     private void writeRef(final int code) throws IOException {
646         final int val = code;
647         if (val < 256) {
648             output.writeByte(MagnesiumValue.STRING_REF_1B);
649             output.writeByte(val);
650         } else if (val < 65792) {
651             output.writeByte(MagnesiumValue.STRING_REF_2B);
652             output.writeShort(val - 256);
653         } else {
654             output.writeByte(MagnesiumValue.STRING_REF_4B);
655             output.writeInt(val);
656         }
657     }
658
659     // Write a lookup module table reference, which table is being referenced is implied by the caller
660     private void writeModuleRef(final int code) throws IOException {
661         final int val = code;
662         if (val < 256) {
663             output.writeByte(MagnesiumValue.MODREF_1B);
664             output.writeByte(val);
665         } else if (val < 65792) {
666             output.writeByte(MagnesiumValue.MODREF_2B);
667             output.writeShort(val - 256);
668         } else {
669             output.writeByte(MagnesiumValue.MODREF_4B);
670             output.writeInt(val);
671         }
672     }
673 }