package com.github.rschmitt.dynamicobject;

import java.io.IOException;

import org.fressian.Writer;
import org.fressian.handlers.WriteHandler;

public class FressianWriteHandler<D extends DynamicObject<D>> implements WriteHandler {
    private final Class<D> type;
    private final String tag;

    public FressianWriteHandler(Class<D> type, String tag) {
        this.type = type;
        this.tag = tag;
    }

    @Override
    @SuppressWarnings("unchecked")
    public void write(Writer w, Object instance) throws IOException {
        w.writeTag(tag, 1);
        w.writeObject(((DynamicObject) instance).getMap());
    }
}

@@ -48,29 +48,73 @@
 ort 
-java.io.IOException;%0A
+lombok.RequiredArgsConstructor;%0Aimport org.fressian.CachedObject;
 %0Aimp
@@ -182,16 +182,263 @@
 ndler;%0A%0A
+import javax.annotation.concurrent.Immutable;%0Aimport javax.annotation.concurrent.NotThreadSafe;%0Aimport java.io.IOException;%0Aimport java.util.AbstractCollection;%0Aimport java.util.Iterator;%0Aimport java.util.Map;%0Aimport java.util.function.Function;%0A%0A
 public c
@@ -717,43 +717,8 @@
 ide%0A
-    @SuppressWarnings(%22unchecked%22)%0A
     
@@ -795,87 +795,2572 @@
     
-w.writeTag(tag, 1);%0A        w.writeObject(((DynamicObject) instance).getMap());
+// We manually serialize the backing map so that we can apply caching transformations to specific subcomponents.%0A        // To avoid needless copying we do this via an adapter rather than copying to a temporary list.%0A        w.writeTag(tag, 1);%0A        w.writeTag(%22map%22, 1);%0A%0A        Map map = ((DynamicObject) instance).getMap();%0A        w.writeList(new TransformedMap(map, this::transformKey));%0A    %7D%0A%0A    /*%0A     * Although Fressian will automatically cache the string components of each Keyword, by default we still spend a%0A     * minimum of three bytes per keyword - one for the keyword directive itself, one for the namespace (usually null),%0A     * and one for the cached reference to the keyword's string. By requesting caching of the Keyword object itself like%0A     * this, we can get this down to one byte (after the initial cache miss).%0A     *%0A     * The downside of this is that cache misses incur additional an additional byte marking the key as being LRU-cache%0A     * capable, and the cache misses will also result in two cache entries being introduced, causing more cache churn%0A     * than there would be otherwise.%0A     */%0A    private Object transformKey(Object key) %7B%0A        return new CachedObject(key);%0A    %7D%0A%0A    @Immutable%0A    @RequiredArgsConstructor%0A    @SuppressWarnings(%22unchecked%22)%0A    private static class TransformedMap extends AbstractCollection %7B%0A        private final Map backingMap;%0A        private final Function%3CObject, Object%3E keysTransformation;%0A%0A        @Override%0A        public Iterator iterator() %7B%0A            return new TransformingKeyValueIterator(backingMap.entrySet().iterator(), keysTransformation);%0A        %7D%0A%0A        @Override%0A        public int size() %7B%0A            return backingMap.size() * 2;%0A        %7D%0A    %7D%0A%0A    @NotThreadSafe%0A    @RequiredArgsConstructor%0A    private static class TransformingKeyValueIterator implements Iterator %7B%0A        private final Iterator%3CMap.Entry%3E entryIterator;%0A        private final Function%3CObject, Object%3E keysTransformation;%0A%0A        Object pendingValue = null;%0A%0A        @Override%0A        public boolean hasNext() %7B%0A            return pendingValue != null %7C%7C entryIterator.hasNext();%0A        %7D%0A%0A        @Override%0A        public Object next() %7B%0A            if (pendingValue != null) %7B%0A                Object value = pendingValue;%0A                pendingValue = null;%0A%0A                return value;%0A            %7D%0A%0A            Map.Entry entry = entryIterator.next();%0A            pendingValue = entry.getValue();%0A%0A            return keysTransformation.apply(entry.getKey());%0A        %7D
 %0A   

Improve Fressian caching performance