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