/* (c) 2017 Open Source Geospatial Foundation - all rights reserved * This code is licensed under the GPL 2.0 license, available at the root * application directory. */ package org.geoserver.catalog.impl; import java.lang.reflect.Proxy; import java.util.ArrayList; import java.util.Collection; import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentSkipListMap; import java.util.function.Function; import java.util.function.Predicate; import org.geoserver.catalog.CatalogInfo; import org.opengis.feature.type.Name; /** * A support index for {@link DefaultCatalogFacade}, can perform fast lookups of {@link CatalogInfo} objects * by id or by "name", where the name is defined by a a user provided mapping function. * * The lookups by predicate have been tested and optimized for performance, in particular * the current for loops turned out to be significantly faster than building and returning streams * * @param <T> */ class CatalogInfoLookup<T extends CatalogInfo> { ConcurrentHashMap<Class<T>, Map<String, T>> idMultiMap = new ConcurrentHashMap<>(); ConcurrentHashMap<Class<T>, Map<Name, T>> nameMultiMap = new ConcurrentHashMap<>(); Function<T, Name> nameMapper; static final Predicate TRUE = x -> true; public CatalogInfoLookup(Function<T, Name> nameMapper) { super(); this.nameMapper = nameMapper; } <K> Map<K, T> getMapForValue(ConcurrentHashMap<Class<T>, Map<K, T>> maps, T value) { Class<T> vc; if(Proxy.isProxyClass(value.getClass())) { ModificationProxy h = (ModificationProxy) Proxy.getInvocationHandler(value); Object po = (T) h.getProxyObject(); vc = (Class<T>) po.getClass(); } else { vc = (Class<T>) value.getClass(); } return getMapForValue(maps, vc); } protected <K> Map<K, T> getMapForValue(ConcurrentHashMap<Class<T>, Map<K, T>> maps, Class vc) { Map<K, T> vcMap = maps.get(vc); if(vcMap == null) { vcMap = maps.computeIfAbsent(vc, k -> new ConcurrentSkipListMap<K, T>()); } return vcMap; } public T add(T value) { if(Proxy.isProxyClass(value.getClass())) { ModificationProxy h = (ModificationProxy) Proxy.getInvocationHandler(value); value = (T) h.getProxyObject(); } Map<Name, T> nameMap = getMapForValue(nameMultiMap, value); Name name = nameMapper.apply(value); nameMap.put(name, value); Map<String, T> idMap = getMapForValue(idMultiMap, value); return idMap.put(value.getId(), value); } public Collection<T> values() { List<T> result = new ArrayList<>(); for (Map<String, T> v : idMultiMap.values()) { result.addAll(v.values()); } return result; } public T remove(T value) { Name name = nameMapper.apply(value); Map<Name, T> nameMap = getMapForValue(nameMultiMap, value); nameMap.remove(name); Map<String, T> idMap = getMapForValue(idMultiMap, value); return idMap.remove(value.getId()); } /** * Updates the value in the name map. The new value must be a ModificationProxy */ public void update(T proxiedValue) { ModificationProxy h = (ModificationProxy) Proxy.getInvocationHandler(proxiedValue); T actualValue = (T) h.getProxyObject(); Name oldName = nameMapper.apply(actualValue); Name newName = nameMapper.apply(proxiedValue); if(!oldName.equals(newName)) { Map<Name, T> nameMap = getMapForValue(nameMultiMap, actualValue); nameMap.remove(oldName); nameMap.put(newName, actualValue); } } public void clear() { idMultiMap.clear(); nameMultiMap.clear(); } /** * Looks up objects by class and matching predicate. * * This method is significantly faster than creating a stream and the applying the predicate * on it. Just using this approach instead of the stream makes the overall startup of GeoServer with 20k * layers go down from 50s to 44s (which is a lot, considering there is a lot of other things going on) * @param clazz * @param predicate * @return */ <U extends CatalogInfo> List<U> list(Class<U> clazz, Predicate<U> predicate) { ArrayList<U> result = new ArrayList<U>(); for (Class<T> key : nameMultiMap.keySet()) { if (clazz.isAssignableFrom(key)) { Map<Name, T> valueMap = nameMultiMap.get(key); if (valueMap != null) { for (T v : valueMap.values()) { final U u = (U) v; if (predicate == TRUE || predicate.test(u)) { result.add(u); } } } } } return result; } /** * Looks up a CatalogInfo by class and identifier * @param id * @param clazz * @return */ public <U extends CatalogInfo> U findById(String id, Class<U> clazz) { for (Class<T> key : idMultiMap.keySet()) { if (clazz.isAssignableFrom(key)) { Map<String, T> valueMap = idMultiMap.get(key); if(valueMap != null) { T t = valueMap.get(id); if(t != null) { return (U) t; } } } } return null; } /** * Looks up a CatalogInfo by class and name * @param clazz * @param id * @return */ public <U extends CatalogInfo> U findByName(Name name, Class<U> clazz) { for (Class<T> key : nameMultiMap.keySet()) { if (clazz.isAssignableFrom(key)) { Map<Name, T> valueMap = nameMultiMap.get(key); if(valueMap != null) { T t = valueMap.get(name); if(t != null) { return (U) t; } } } } return null; } /** * Looks up objects by class and matching predicate. * * This method is significantly faster than creating a stream and the applying the predicate * on it. Just using this approach instead of the stream makes the overall startup of GeoServer with 20k * layers go down from 50s to 44s (which is a lot, considering there is a lot of other things going on) * @param clazz * @param predicate * @return */ <U extends CatalogInfo> U findFirst(Class<U> clazz, Predicate<U> predicate) { for (Class<T> key : nameMultiMap.keySet()) { if (clazz.isAssignableFrom(key)) { Map<Name, T> valueMap = nameMultiMap.get(key); if (valueMap != null) { for (T v : valueMap.values()) { final U u = (U) v; if (predicate == TRUE || predicate.test(u)) { return u; } } } } } return null; } }