DirtyFlagMap.java

/*
 * All content copyright Terracotta, Inc., unless otherwise indicated. All rights reserved.
 * Copyright IBM Corp. 2024, 2025
 *
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
 * use this file except in compliance with the License. You may obtain a copy
 * of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations
 * under the License.
 *
 */

package org.quartz.utils;

import java.lang.reflect.Array;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;

/**
 * <p>
 * An implementation of <code>Map</code> that wraps another <code>Map</code>
 * and flags itself 'dirty' when it is modified.
 * </p>
 *
 * @author James House
 */
public class DirtyFlagMap<K,V> implements Map<K,V>, Cloneable, java.io.Serializable {

    /*
     * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
     *
     * Data members.
     *
     * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
     */
    private static final long serialVersionUID = 1433884852607126222L;

    private boolean dirty = false;
    private Map<K,V> map;

    /*
     * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
     *
     * Constructors.
     *
     * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
     */

    /**
     * <p>
     * Create a DirtyFlagMap that 'wraps' a <code>HashMap</code>.
     * </p>
     *
     * @see java.util.HashMap
     */
    public DirtyFlagMap() {
        map = new HashMap<>();
    }

    /**
     * <p>
     * Create a DirtyFlagMap that 'wraps' a <code>HashMap</code> that has the
     * given initial capacity.
     * </p>
     *
     * @see java.util.HashMap
     */
    public DirtyFlagMap(final int initialCapacity) {
        map = new HashMap<>(initialCapacity);
    }

    /**
     * <p>
     * Create a DirtyFlagMap that 'wraps' a <code>HashMap</code> that has the
     * given initial capacity and load factor.
     * </p>
     *
     * @see java.util.HashMap
     */
    public DirtyFlagMap(final int initialCapacity, final float loadFactor) {
        map = new HashMap<>(initialCapacity, loadFactor);
    }

    /*
     * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
     *
     * Interface.
     *
     * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
     */

    /**
     * <p>
     * Clear the 'dirty' flag (set dirty flag to <code>false</code>).
     * </p>
     */
    public void clearDirtyFlag() {
        dirty = false;
    }

    /**
     * <p>
     * Determine whether the <code>Map</code> is flagged dirty.
     * </p>
     */
    public boolean isDirty() {
        return dirty;
    }

    /**
     * <p>
     * Get a direct handle to the underlying Map.
     * </p>
     */
    public Map<K,V> getWrappedMap() {
        return map;
    }

    public void clear() {
        if (!map.isEmpty()) {
            dirty = true;
        }
        map.clear();
    }

    public boolean containsKey(final Object key) {
        return map.containsKey(key);
    }

    public boolean containsValue(final Object val) {
        return map.containsValue(val);
    }

    public Set<Entry<K,V>> entrySet() {
        return new DirtyFlagMapEntrySet(map.entrySet());
    }

    @Override
    public boolean equals(final Object obj) {
        if (obj == null || !(obj instanceof DirtyFlagMap)) {
            return false;
        }

        return map.equals(((DirtyFlagMap<?,?>) obj).getWrappedMap());
    }

    @Override
    public int hashCode()
    {
        return map.hashCode();
    }

    public V get(final Object key) {
        return map.get(key);
    }

    public boolean isEmpty() {
        return map.isEmpty();
    }

    public Set<K> keySet() {
        return new DirtyFlagSet<>(map.keySet());
    }

    public V put(final K key, final V val) {
        dirty = true;

        return map.put(key, val);
    }

    public void putAll(final Map<? extends K, ? extends V> t) {
        if (!t.isEmpty()) {
            dirty = true;
        }

        map.putAll(t);
    }

    public V remove(final Object key) {
        V obj = map.remove(key);

        if (obj != null) {
            dirty = true;
        }

        return obj;
    }

    public int size() {
        return map.size();
    }

    public Collection<V> values() {
        return new DirtyFlagCollection<>(map.values());
    }

    @Override
    @SuppressWarnings("unchecked") // suppress warnings on generic cast of super.clone() and map.clone() lines.
    public Object clone() {
        DirtyFlagMap<K,V> copy;
        try {
            copy = (DirtyFlagMap<K,V>) super.clone();
            if (map instanceof HashMap) {
                copy.map = (Map<K,V>)((HashMap<K,V>)map).clone();
            }
        } catch (CloneNotSupportedException ex) {
            throw new IncompatibleClassChangeError("Not Cloneable.");
        }

        return copy;
    }

    /**
     * Wrap a Collection so we can mark the DirtyFlagMap as dirty if
     * the underlying Collection is modified.
     */
    private class DirtyFlagCollection<T> implements Collection<T> {
        private final Collection<T> collection;

        public DirtyFlagCollection(final Collection<T> c) {
            collection = c;
        }

        protected Collection<T> getWrappedCollection() {
            return collection;
        }

        public Iterator<T> iterator() {
            return new DirtyFlagIterator<>(collection.iterator());
        }

        public boolean remove(final Object o) {
            boolean removed = collection.remove(o);
            if (removed) {
                dirty = true;
            }
            return removed;
        }

        public boolean removeAll(final Collection<?> c) {
            boolean changed = collection.removeAll(c);
            if (changed) {
                dirty = true;
            }
            return changed;
        }

        public boolean retainAll(final Collection<?> c) {
            boolean changed = collection.retainAll(c);
            if (changed) {
                dirty = true;
            }
            return changed;
        }

        public void clear() {
            if (!collection.isEmpty()) {
                dirty = true;
            }
            collection.clear();
        }

        // Pure wrapper methods
        public int size() { return collection.size(); }
        public boolean isEmpty() { return collection.isEmpty(); }
        public boolean contains(final Object o) { return collection.contains(o); }
        public boolean add(final T o) { return collection.add(o); } // Not supported
        public boolean addAll(final Collection<? extends T> c) { return collection.addAll(c); } // Not supported
        public boolean containsAll(final Collection<?> c) { return collection.containsAll(c); }
        public Object[] toArray() { return collection.toArray(); }
        public <U> U[] toArray(final U[] array) { return collection.toArray(array); }
    }

    /**
     * Wrap a Set so we can mark the DirtyFlagMap as dirty if
     * the underlying Collection is modified.
     */
    private class DirtyFlagSet<T> extends DirtyFlagCollection<T> implements Set<T> {
        public DirtyFlagSet(final Set<T> set) {
            super(set);
        }

        protected Set<T> getWrappedSet() {
            return (Set<T>)getWrappedCollection();
        }
    }

    /**
     * Wrap an Iterator so that we can mark the DirtyFlagMap as dirty if an
     * element is removed.
     */
    private class DirtyFlagIterator<T> implements Iterator<T> {
        private final Iterator<T> iterator;

        public DirtyFlagIterator(final Iterator<T> iterator) {
            this.iterator = iterator;
        }

        public void remove() {
            dirty = true;
            iterator.remove();
        }

        // Pure wrapper methods
        public boolean hasNext() { return iterator.hasNext(); }
        public T next() { return iterator.next(); }
    }

    /**
     * Wrap a Map.Entry Set so we can mark the Map as dirty if
     * the Set is modified, and return Map.Entry objects
     * wrapped in the <code>DirtyFlagMapEntry</code> class.
     */
    private class DirtyFlagMapEntrySet extends DirtyFlagSet<Map.Entry<K,V>> {

        public DirtyFlagMapEntrySet(final Set<Map.Entry<K,V>> set) {
            super(set);
        }

        @Override
        public Iterator<Map.Entry<K,V>> iterator() {
            return new DirtyFlagMapEntryIterator(getWrappedSet().iterator());
        }

        @Override
        public Object[] toArray() {
            return toArray(new Object[super.size()]);
        }

        @SuppressWarnings("unchecked") // suppress warnings on both U[] and U casting.
        @Override
        public <U> U[] toArray(final U[] array) {
            if (!array.getClass().getComponentType().isAssignableFrom(Entry.class)) {
                throw new IllegalArgumentException("Array must be of type assignable from Map.Entry");
            }

            int size = super.size();

            U[] result =
                array.length < size ?
                    (U[])Array.newInstance(array.getClass().getComponentType(), size) : array;

            Iterator<Map.Entry<K,V>> entryIter = iterator(); // Will return DirtyFlagMapEntry objects
            for (int i = 0; i < size; i++) {
                result[i] = ( U ) entryIter.next();
            }

            if (result.length > size) {
                result[size] = null;
            }

            return result;
        }
    }

    /**
     * Wrap an Iterator over Map.Entry objects so that we can
     * mark the Map as dirty if an element is removed or modified.
     */
    private class DirtyFlagMapEntryIterator extends DirtyFlagIterator<Map.Entry<K,V>> {
        public DirtyFlagMapEntryIterator(final Iterator<Map.Entry<K,V>> iterator) {
            super(iterator);
        }

        @Override
        public DirtyFlagMapEntry next() {
            return new DirtyFlagMapEntry(super.next());
        }
    }

    /**
     * Wrap a Map.Entry so we can mark the Map as dirty if
     * a value is set.
     */
    private class DirtyFlagMapEntry implements Map.Entry<K,V> {
        private final Map.Entry<K,V> entry;

        public DirtyFlagMapEntry(final Map.Entry<K,V> entry) {
            this.entry = entry;
        }

        public V setValue(final V o) {
            dirty = true;
            return entry.setValue(o);
        }

        // Pure wrapper methods
        public K getKey() { return entry.getKey(); }
        public V getValue() { return entry.getValue(); }
        public boolean equals(Object o) { return entry.equals(o); }
    }
}