All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.mapdb.Bind Maven / Gradle / Ivy

The newest version!
/*
 *  Copyright (c) 2012 Jan Kotek
 *
 *  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.mapdb;

import java.util.*;
import java.util.concurrent.ConcurrentMap;

/**
 * Binding is simple yet powerful way to keep secondary collection synchronized with primary collection.
 * Primary collection provides notification on updates and secondary collection is modified accordingly.
 * This way MapDB provides secondary indexes, values and keys. It also supports less usual scenarious such
 * as histograms, inverse lookup index (on maps), group counters and so on.
 *
 * There are two things to keep on mind when using binding:
 *
 *  * Binding is not persistent, so it needs to be restored every time store is reopened.
 *    If you modify primary collection before binding is restored, secondary collection does not get updated and becomes
 *    inconsistent.
 *
 *  * If secondary collection is empty, binding will recreate its content based on primary collection.
 *    If there is even single item on secondary collection, binding assumes it is consistent and leaves it as its.
 *
 *  Any thread-safe collection can be used as secondary (not just collections provided by MapDB).
 *  This gives great flexibility for modeling
 *  and scaling your data. For example primary data can be stored in durable DB with transactions and large secondary
 *  indexes may be stored in other faster non-durable DB. Or primary collection may be stored on disk and smaller
 *  secondary index (such as category counters) can be stored in memory for faster lookups. Also you may use
 *  ordinary `java.util.*` collections (if they are thread safe) to get additional speed.
 *
 *  There are many [code examples](https://github.com/jankotek/MapDB/tree/master/src/test/java/examples)
 *  how Collection Binding can be used.
 *
 * @author Jan Kotek
 */
public final class Bind {

    private Bind(){}



    /**
     * Listener called when `Map` is modified.
     * @param  key type  in map
     * @param  value type in map
     */
    public interface MapListener{
        /**
         * Callback method called after `Map` was modified.
         * It is called on insert, update or delete.
         *
         * MapDB collections do not support null keys or values.
         * Null parameter may be than used to indicate operation:
         *
         *
         *
         * @param key key in map
         * @param oldVal old value in map (if any, null on inserts)
         * @param newVal new value in map (if any, null on deletes)
         */
        void update(K key, V oldVal, V newVal);
    }

    /**
     * Primary Maps must provide notifications when it is modified.
     * So Primary Maps must implement this interface to allow registering callback listeners.
     *
     * @param  key type  in map
     * @param  value type in map
     */
    public interface MapWithModificationListener extends Map {
        /**
         * Add new modification listener notified when Map has been updated
         * @param listener callback interface notified when map changes
         */
        public void addModificationListener(MapListener listener);

        /**
         * Remove registered notification listener
         *
         * @param listener  callback interface notified when map changes
         */
        public void removeModificationListener(MapListener listener);


        /**
         *
         * @return size of map, but in  64bit long which does not overflow at 2e9 items.
         */
        public long sizeLong();
    }

    /**
     * Binds {@link Atomic.Long} to Primary Map so the Atomic.Long contains size of Map.
     * `Atomic.Long` is incremented on each insert and decremented on each entry removal.
     * MapDB collections usually do not keep their size, but require complete traversal to count items.
     *
     * If `Atomic.Long` has zero value, it will be updated with value from `map.size()` and than
     * bind to map.
     *
     * This binding is not persistent. You need to restore it every time store is reopened.
     *
     * NOTE: {@link BTreeMap} and {@link HTreeMap} already supports this directly as optional parameter named `counter`.
     * In that case all calls to `Map.size()` are forwarded to underlying counter. Check parameters at
     * {@link DB#createHashMap(String)} and
     * {@link DB#createTreeMap(String)}
     *
     *
     * @param map primary map whose size needs to be tracked
     * @param sizeCounter number updated when Map Entry is added or removed.
     */
    public static void size(MapWithModificationListener map, final Atomic.Long sizeCounter){
        //set initial value first if necessary
        if(sizeCounter.get() == 0){
            long size = map.sizeLong();
            if(sizeCounter.get()!=size)
                sizeCounter.set(size);
        }

        map.addModificationListener(new MapListener() {
            @Override
            public void update(Object key, Object oldVal, Object newVal) {
                if(oldVal == null && newVal!=null){
                    sizeCounter.incrementAndGet();
                }else if(oldVal!=null && newVal == null){
                    sizeCounter.decrementAndGet();
                }

                //update does not change collection size
            }
        });
    }

    /**
     * Binds Secondary Map so that it contains Key from Primary Map and custom Value.
     * Secondary Value is updated every time Primary Map is modified.
     *
     * If Secondary Map is empty its content will be recreated from Primary Map.
     * This binding is not persistent. You need to restore it every time store is reopened.
     *
     * @param map Primary Map
     * @param secondary Secondary Map with custom
     * @param fun function which calculates secondary value from primary key and value
     * @param  key type in primary and Secondary Map
     * @param  value type in Primary Map
     * @param  value type in Secondary Map.
     */
    public static  void secondaryValue(MapWithModificationListener map,
                                              final Map secondary,
                                              final Fun.Function2 fun){
        //fill if empty
        if(secondary.isEmpty()){
            for(Map.Entry e:map.entrySet())
                secondary.put(e.getKey(), fun.run(e.getKey(),e.getValue()));
        }
        //hook listener
        map.addModificationListener(new MapListener() {
            @Override
            public void update(K key, V oldVal, V newVal) {
                if(newVal == null){
                    //removal
                    secondary.remove(key);
                }else{
                    secondary.put(key, fun.run(key,newVal));
                }
            }
        });
    }

    /**
     * Binds Secondary Map so that it contains Key from Primary Map and custom Value.
     * Secondary Value is updated every time Primary Map is modified.
     *
     * If Secondary Map is empty its content will be recreated from Primary Map.
     * This binding is not persistent. You need to restore it every time store is reopened.
     *
     * @param map Primary Map
     * @param secondary Secondary Map with custom
     * @param fun function which calculates secondary values from primary key and value
     * @param  key type in primary and Secondary Map
     * @param  value type in Primary Map
     * @param  value type in Secondary Map.
     */
    public static  void secondaryValues(MapWithModificationListener map,
                                                final Set> secondary,
                                                final Fun.Function2 fun){
        //fill if empty
        if(secondary.isEmpty()){
            for(Map.Entry e:map.entrySet()){
                V2[] v = fun.run(e.getKey(),e.getValue());
                if(v!=null)
                    for(V2 v2:v)
                        secondary.add(Fun.t2(e.getKey(), v2));
            }
        }
        //hook listener
        map.addModificationListener(new MapListener() {
            @Override
            public void update(K key, V oldVal, V newVal) {
                if(newVal == null){
                    //removal
                    V2[] v = fun.run(key,oldVal);
                    if(v != null)
                        for(V2 v2 :v)
                            secondary.remove(Fun.t2(key,v2));
                }else if(oldVal==null){
                    //insert
                    V2[] v = fun.run(key,newVal);
                    if(v != null)
                        for(V2 v2 :v)
                            secondary.add(Fun.t2(key,v2));
                }else{
                    //update, must remove old key and insert new
                    V2[] oldv = fun.run(key, oldVal);
                    V2[] newv = fun.run(key, newVal);
                    if(oldv==null){
                        //insert new
                        if(newv!=null)
                            for(V2 v :newv)
                                secondary.add(Fun.t2(key,v));
                        return;
                    }
                    if(newv==null){
                        //remove old
                        for(V2 v :oldv)
                            secondary.remove(Fun.t2(key,v));
                        return;
                    }

                    Set hashes = new HashSet();
                    for(V2 v:oldv)
                        hashes.add(v);

                    //add new non existing items
                    for(V2 v:newv){
                        if(!hashes.contains(v)){
                            secondary.add(Fun.t2(key,v));
                        }
                    }
                    //remove items which are in old, but not in new
                    for(V2 v:newv){
                        hashes.remove(v);
                    }
                    for(V2 v:hashes){
                        secondary.remove(Fun.t2(key,v));
                    }
                }
            }
        });
    }


    /**
     * Binds Secondary Set so it contains Secondary Key (Index). Usefull if you need
     * to lookup Keys from Primary Map by custom criteria. Other use is for reverse lookup
     *
     * To lookup keys in Secondary Set use {@link Bind#findSecondaryKeys(java.util.NavigableSet, Object)}
     *
     *
     * If Secondary Set is empty its content will be recreated from Primary Map.
     * This binding is not persistent. You need to restore it every time store is reopened.
     *
     * @param map primary map
     * @param secondary secondary set
     * @param fun function which calculates Secondary Key from Primary Key and Value
     * @param  Key in Primary Map
     * @param  Value in Primary Map
     * @param  Secondary
     */
    public static  void secondaryKey(MapWithModificationListener map,
                                                final Set> secondary,
                                                final Fun.Function2 fun){
        //fill if empty
        if(secondary.isEmpty()){
            for(Map.Entry e:map.entrySet()){
                secondary.add(Fun.t2(fun.run(e.getKey(),e.getValue()), e.getKey()));
            }
        }
        //hook listener
        map.addModificationListener(new MapListener() {
            @Override
            public void update(K key, V oldVal, V newVal) {
                if(newVal == null){
                    //removal
                    secondary.remove(Fun.t2(fun.run(key, oldVal), key));
                }else if(oldVal==null){
                    //insert
                    secondary.add(Fun.t2(fun.run(key,newVal), key));
                }else{
                    //update, must remove old key and insert new
                    K2 oldKey = fun.run(key, oldVal);
                    K2 newKey = fun.run(key, newVal);
                    if(oldKey == newKey || oldKey.equals(newKey)) return;
                    secondary.remove(Fun.t2(oldKey, key));
                    secondary.add(Fun.t2(newKey,key));
                }
            }
        });
    }

    /**
     * Binds Secondary Set so it contains Secondary Key (Index). Usefull if you need
     * to lookup Keys from Primary Map by custom criteria. Other use is for reverse lookup
     *
     * To lookup keys in Secondary Set use {@link Bind#findSecondaryKeys(java.util.NavigableSet, Object)}
     *
     *
     * If Secondary Set is empty its content will be recreated from Primary Map.
     * This binding is not persistent. You need to restore it every time store is reopened.
     *
     * @param map primary map
     * @param secondary secondary set
     * @param fun function which calculates Secondary Key from Primary Key and Value
     * @param  Key in Primary Map
     * @param  Value in Primary Map
     * @param  Secondary
     */
    public static  void secondaryKey(MapWithModificationListener map,
                                              final Map secondary,
                                              final Fun.Function2 fun){
        //fill if empty
        if(secondary.isEmpty()){
            for(Map.Entry e:map.entrySet()){
                secondary.put(fun.run(e.getKey(), e.getValue()), e.getKey());
            }
        }
        //hook listener
        map.addModificationListener(new MapListener() {
            @Override
            public void update(K key, V oldVal, V newVal) {
                if(newVal == null){
                    //removal
                    secondary.remove(fun.run(key, oldVal));
                }else if(oldVal==null){
                    //insert
                    secondary.put(fun.run(key,newVal), key);
                }else{
                    //update, must remove old key and insert new
                    K2 oldKey = fun.run(key, oldVal);
                    K2 newKey = fun.run(key, newVal);
                    if(oldKey == newKey || oldKey.equals(newKey)) return;
                    secondary.remove(oldKey);
                    secondary.put(newKey,key);
                }
            }
        });
    } 
    /**
     * Binds Secondary Set so it contains Secondary Key (Index). Useful if you need
     * to lookup Keys from Primary Map by custom criteria. Other use is for reverse lookup
     *
     * To lookup keys in Secondary Set use {@link Bind#findSecondaryKeys(java.util.NavigableSet, Object)}
     *
     *
     * If Secondary Set is empty its content will be recreated from Primary Map.
     * This binding is not persistent. You need to restore it every time store is reopened.
     *
     * @param map primary map
     * @param secondary secondary set
     * @param fun function which calculates Secondary Keys from Primary Key and Value
     * @param  Key in Primary Map
     * @param  Value in Primary Map
     * @param  Secondary
     */
    public static  void secondaryKeys(MapWithModificationListener map,
                                              final Set> secondary,
                                              final Fun.Function2 fun){
        //fill if empty
        if(secondary.isEmpty()){
            for(Map.Entry e:map.entrySet()){
                K2[] k2 = fun.run(e.getKey(), e.getValue());
                if(k2 != null)
                    for(K2 k22 :k2)
                        secondary.add(Fun.t2(k22, e.getKey()));
            }
        }
        //hook listener
        map.addModificationListener(new MapListener() {
            @Override
            public void update(K key, V oldVal, V newVal) {
                if(newVal == null){
                    //removal
                    K2[] k2 = fun.run(key,oldVal);
                    if(k2 != null)
                        for(K2 k22 :k2)
                            secondary.remove(Fun.t2(k22, key));
                }else if(oldVal==null){
                    //insert
                    K2[] k2 = fun.run(key,newVal);
                    if(k2 != null)
                        for(K2 k22 :k2)
                            secondary.add(Fun.t2(k22, key));
                }else{
                    //update, must remove old key and insert new
                    K2[] oldk = fun.run(key, oldVal);
                    K2[] newk = fun.run(key, newVal);
                    if(oldk==null){
                        //insert new
                        if(newk!=null)
                            for(K2 k22 :newk)
                                secondary.add(Fun.t2(k22, key));
                        return;
                    }
                    if(newk==null){
                        //remove old
                        for(K2 k22 :oldk)
                            secondary.remove(Fun.t2(k22, key));
                        return;
                    }

                    Set hashes = new HashSet();
                    for(K2 k:oldk)
                        hashes.add(k);

                    //add new non existing items
                    for(K2 k2:newk){
                        if(!hashes.contains(k2)){
                            secondary.add(Fun.t2(k2, key));
                        }
                    }
                    //remove items which are in old, but not in new
                    for(K2 k2:newk){
                        hashes.remove(k2);
                    }
                    for(K2 k2:hashes){
                        secondary.remove(Fun.t2(k2, key));
                    }
                }
            }
        });
    }

    /**
     * Binds Secondary Set so it contains inverse mapping to Primary Map: Primary Value will become Secondary Key.
     * This is useful for creating bi-directional Maps.
     *
     * To lookup keys in Secondary Set use {@link Bind#findSecondaryKeys(java.util.NavigableSet, Object)}
     *
     * If Secondary Set is empty its content will be recreated from Primary Map.
     * This binding is not persistent. You need to restore it every time store is reopened.
     *
     * @param primary Primary Map for which inverse mapping will be created
     * @param inverse Secondary Set which will contain inverse mapping
     * @param  Key in Primary Map and Second Value in Secondary Set
     * @param  Value in Primary Map and Primary Value in Secondary Set
     */
    public static  void mapInverse(MapWithModificationListener primary,
                                        Set> inverse) {
        Bind.secondaryKey(primary,inverse, new Fun.Function2(){
            @Override public V run(K key, V value) {
                return value;
            }
        });
    }

    /**
     * Binds Secondary Set so it contains inverse mapping to Primary Map: Primary Value will become Secondary Key.
     * This is useful for creating bi-directional Maps.
     *
     * To lookup keys in Secondary Set use {@link Bind#findSecondaryKeys(java.util.NavigableSet, Object)}
     *
     * If Secondary Set is empty its content will be recreated from Primary Map.
     * This binding is not persistent. You need to restore it every time store is reopened.
     *
     * @param primary Primary Map for which inverse mapping will be created
     * @param inverse Secondary Set which will contain inverse mapping
     * @param  Key in Primary Map and Second Value in Secondary Set
     * @param  Value in Primary Map and Primary Value in Secondary Set
     */
    public static  void mapInverse(MapWithModificationListener primary,
                                        Map inverse) {
        Bind.secondaryKey(primary,inverse, new Fun.Function2(){
            @Override public V run(K key, V value) {
                return value;
            }
        });
    }







    /**
     * Binds Secondary Map so it it creates [histogram](http://en.wikipedia.org/wiki/Histogram) from
     * data in Primary Map. Histogram keeps count how many items are in each category.
     * This method takes function which defines in what category each Primary Map entry is in.
     *
     *
     * If Secondary Map is empty its content will be recreated from Primary Map.
     * This binding is not persistent. You need to restore it every time store is reopened.
     *
     * @param primary Primary Map to create histrogram for
     * @param histogram Secondary Map to create histogram for, key is Category, value is number of items in category
     * @param entryToCategory returns Category in which entry from Primary Map belongs to.
     * @param  Key type in primary map
     * @param  Value type in primary map
     * @param  Category type
     */
    public static  void histogram(MapWithModificationListener primary, final ConcurrentMap histogram,
                                  final Fun.Function2 entryToCategory){

        MapListener listener = new MapListener() {
            @Override public void update(K key, V oldVal, V newVal) {
                if(newVal == null){
                    //removal
                    C category = entryToCategory.run(key,oldVal);
                    incrementHistogram(category, -1);
                }else if(oldVal==null){
                    //insert
                    C category = entryToCategory.run(key,newVal);
                    incrementHistogram(category, 1);
                }else{
                    //update, must remove old key and insert new
                    C oldCat = entryToCategory.run(key, oldVal);
                    C newCat = entryToCategory.run(key, newVal);
                    if(oldCat == newCat || oldCat.equals(newCat)) return;
                    incrementHistogram(oldCat,-1);
                    incrementHistogram(oldCat,1);
                }

            }

            /** atomically update counter in histogram*/
            private void incrementHistogram(C category, long i) {
                for(;;){
                    Long oldCount = histogram.get(category);
                    if(oldCount == null){
                        //insert new count
                        if(histogram.putIfAbsent(category,i) == null)
                            return;
                    }else{
                        //increase existing count
                        Long newCount = oldCount+i;
                        if(histogram.replace(category,oldCount, newCount))
                            return;
                    }
                }
            }
        };

        primary.addModificationListener(listener);
    }




}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy