Thursday, April 11, 2013

Map view of list key-value pairs in Java

Long time no code.

Example use

import ListMap;

import javax.annotation.Nonnull;
import java.util.ArrayList;

import static java.util.Arrays.asList;

public class AttributesMap
        extends ListMap<FooItem, String, String> {
    public AttributesMap(final FooOperations foos, final String cookie) {
        super(new FooItems(foos, cookie), new FooItemConverter());
    }

    private static class FooItems
            extends ArrayList<FooItem> {
        private final FooOperations foos;
        private final String cookie;

        private FooItems(final FooOperations foos, final String cookie) {
            super(asList(foos.getAttributes(cookie)));
            this.foos = foos;
            this.cookie = cookie;
        }

        @Override
        public FooItem set(final int index, final FooItem element) {
            final FooItem oldElement = super.set(index, element);
            save();
            return oldElement;
        }

        @Override
        public void add(final int index, final FooItem element) {
            super.add(index, element);
            save();
        }

        @Override
        public FooItem remove(final int index) {
            final FooItem oldElement = super.remove(index);
            save();
            return oldElement;
        }

        private void save() {
            try {
                foos.setAttributes(cookie, toArray(new FooItem[size()]));
            } catch (final BadDataRef e) {
                throw new UserRuntimeException(e);
            }
        }
    }

    private static final class FooItemConverter
            implements Converter<FooItem, String, String> {
        @Nonnull
        @Override
        public FooItem toElement(@Nonnull final String key, final String value) {
            return new FooItem(key, value);
        }

        @Nonnull
        @Override
        public Entry<String, String> toEntry(@Nonnull final FooItem element) {
            return new SimpleEntry<>(element.tag, element.value);
        }
    }
}

ListMap utility class

import javax.annotation.Nonnull;
import javax.annotation.concurrent.NotThreadSafe;
import java.util.AbstractMap;
import java.util.AbstractSet;
import java.util.List;
import java.util.ListIterator;

/**
 * {@code ListMap} provides a read-write map view over a list of elements.  The elements must
 * provide a key-value pair structure; keys must {@link #equals(Object)} and {@link #hashCode()}.
 * <p/>
 * All map mutating methods also mutate the underlying list.  Mutating the underlying list also
 * mutates the map view.
 * <p/>
 * There is no synchronization; instances are not thread safe.
 *
 * @param <T> the type of list elements
 * @param <K> the type of map keys
 * @param <V> the type of map values
 *
 * @author <a href="mailto:binkley@alumni.rice.edu">B. K. Oxley (binkley)</a>
 */
@NotThreadSafe
public class ListMap<T, K, V>
        extends AbstractMap<K, V> {
    private final List<T> elements;
    private final Converter<T, K, V> converter;

    /**
     * Creates a new {@code ListMap} for the given <var>elements</var> and <var>converter</var>.
     *
     * @param elements the list of elements underlying the map, never missing
     * @param converter the converter between elements and entry objects, never missing
     */
    public ListMap(@Nonnull final List<T> elements, @Nonnull final Converter<T, K, V> converter) {
        this.elements = elements;
        this.converter = converter;
    }

    /**
     * {@inheritDoc}
     * <p/>
     * Updates the underlying list of elements.
     */
    @Override
    public V put(final K key, final V value) {
        final ListIterator<T> lit = elements.listIterator();
        while (lit.hasNext()) {
            final T element = lit.next();
            final Entry<K, V> entry = converter.toEntry(element);
            if (entry.getKey().equals(key)) {
                elements.set(lit.nextIndex() - 1, converter.toElement(key, value));
                return entry.getValue();
            }
        }
        lit.add(converter.toElement(key, value));
        return null;
    }

    /**
     * {@inheritDoc}
     *
     * @return a specialized set view of the entries over the element list
     */
    @Override
    @Nonnull
    public ListMapSet entrySet() {
        return new ListMapSet();
    }

    /**
     * Exposes the underlying list of elements backing the map view.
     *
     * @return the element list, never missing
     */
    @Nonnull
    public List<T> elements() {
        return elements;
    }

    /**
     * Converts between elements and entry objects.
     *
     * @param <T> the type of list elements
     * @param <K> the type of map keys
     * @param <V> the type of map values
     */
    public static interface Converter<T, K, V> {
        /**
         * Converts to an element instance from a map <var>key</var>-<var>value</var> pair.
         *
         * @param key the map key, never missing
         * @param value the map value, optional
         *
         * @return the corresponding list element, never missing
         */
        @Nonnull
        T toElement(@Nonnull final K key, final V value);

        /**
         * Converts to a map entry instance from a list <var>element</var>.
         *
         * @param element the list element, never missing
         *
         * @return the corresponding map entry, never missing
         */
        @Nonnull
        Entry<K, V> toEntry(@Nonnull final T element);
    }

    /**
     * Backing set for {@link ListMap} exposed as a separate type so callers may access a {@link
     * #iterator() list iterator} and a list iterator {@link #iterator(int) offset by an index}.
     */
    public final class ListMapSet
            extends AbstractSet<Entry<K, V>> {
        /**
         * {@inheritDoc}
         *
         * @see java.util.List#listIterator()
         */
        @Override
        @Nonnull
        public ListMapIterator iterator() {
            return new ListMapIterator();
        }

        @Override
        public int size() {
            return elements.size();
        }

        @Override
        public boolean add(final Entry<K, V> entry) {
            return elements.add(converter.toElement(entry.getKey(), entry.getValue()));
        }

        /** @see List#listIterator(int) */
        @Nonnull
        public ListMapIterator iterator(final int index) {
            return new ListMapIterator(index);
        }

    }

    /**
     * Entry set iterator for {@link ListMapSet} exposed as a separate type.
     *
     * @see ListMapSet#iterator() list iterator
     * @see ListMapSet#iterator(int) offset by an index
     */
    public final class ListMapIterator
            implements ListIterator<Entry<K, V>> {
        private final ListIterator<T> it;

        public ListMapIterator() {
            it = elements.listIterator();
        }

        public ListMapIterator(final int index) {
            it = elements.listIterator(index);
        }

        @Override
        public boolean hasNext() {
            return it.hasNext();
        }

        @Override
        public Entry<K, V> next() {
            return new ListMapEntry(it.next());
        }

        @Override
        public boolean hasPrevious() {
            return it.hasPrevious();
        }

        @Override
        public Entry<K, V> previous() {
            return new ListMapEntry(it.previous());
        }

        @Override
        public int nextIndex() {
            return it.nextIndex();
        }

        @Override
        public int previousIndex() {
            return it.previousIndex();
        }

        @Override
        public void remove() {
            it.remove();
        }

        @Override
        public void set(final Entry<K, V> entry) {
            it.set(converter.toElement(entry.getKey(), entry.getValue()));
        }

        @Override
        public void add(final Entry<K, V> entry) {
            it.add(converter.toElement(entry.getKey(), entry.getValue()));
        }

        private class ListMapEntry
                implements Entry<K, V> {
            private final T element;

            ListMapEntry(final T element) {
                this.element = element;
            }

            @Override
            public K getKey() {
                return converter.toEntry(element).getKey();
            }

            @Override
            public V getValue() {
                return converter.toEntry(element).getValue();
            }

            @Override
            public V setValue(final V value) {
                final Entry<K, V> oldEntry = converter.toEntry(element);
                elements.set(nextIndex() - 1, converter.toElement(oldEntry.getKey(), value));
                return oldEntry.getValue();
            }
        }
    }
}

No comments: