// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. package com.yahoo.search.query.profile; import com.yahoo.component.provider.FreezableClass; import java.util.Collections; import java.util.HashMap; import java.util.Map; /** * A HashMap wrapper which can be cloned without copying the wrapped map. * Copying of the map is deferred until there is a write access to the wrapped map. * This may be frozen, at which point no further modifications are allowed. * Note that until this is cloned, the internal map may be both read and written. * * @author bratseth */ public class CopyOnWriteContent extends FreezableClass implements Cloneable { // TODO: Now that we used CompiledQueryProfiles at runtime we can remove this // Possible states: // WRITABLE: The map can be freely modified - it is only used by this // -> !isFrozen() && (map!=null || unmodifiableMap==null) // COPYONWRITE: The map is referred by at least one clone - further modification must cause a copy // -> !isFrozen() && (map==null && unmodifiableMap!=null) // FROZEN: No further changes are allowed to the state of this, ever // -> isFrozen() // Possible start states: // WRITABLE: When created using the public constructor // COPYONWRITE: When created by cloning // Possible state transitions: // WRITABLE->COPYONWRITE: When this is cloned // COPYONWRITE->WRITABLE: When a clone is written to // (COPYONWRITE,WRITABLE)->FROZEN: When a profile is frozen /** The modifiable content of this. Null if this is empty or if this is not in the WRITABLE state */ private Map map=null; /** * If map is non-null this is either null (not instantiated yet) or an unmodifiable wrapper of map, * if map is null this is either null (this is empty) or a reference to the map of the content this was cloned from */ private Map unmodifiableMap =null; /** Create a WRITABLE, empty instance */ public CopyOnWriteContent() { } /** Create a COPYONWRITE instance with some initial state */ private static CopyOnWriteContent createInCopyOnWriteState(Map unmodifiableMap) { CopyOnWriteContent content=new CopyOnWriteContent(); content.unmodifiableMap = unmodifiableMap; return content; } /** Create a WRITABLE instance with some initial state */ private static CopyOnWriteContent createInWritableState(Map map) { CopyOnWriteContent content=new CopyOnWriteContent(); content.map = map; return content; } @Override public void freeze() { // Freeze this if (unmodifiableMap==null) unmodifiableMap= map!=null ? Collections.unmodifiableMap(map) : Collections.emptyMap(); map=null; // just to keep the states simpler // Freeze content for (Map.Entry entry : unmodifiableMap.entrySet()) { if (entry.getValue() instanceof QueryProfile) ((QueryProfile)entry.getValue()).freeze(); } super.freeze(); } private boolean isEmpty() { return (map==null || map.isEmpty()) && (unmodifiableMap ==null || unmodifiableMap.isEmpty()); } private boolean isWritable() { return !isFrozen() && (map!=null || unmodifiableMap==null); } @Override public CopyOnWriteContent clone() { if (isEmpty()) return new CopyOnWriteContent(); // No referencing is necessary in this case if (isDeepUnmodifiable(unmodifiableMap())) { // Create an instance pointing to this and put both in the COPYONWRITE state unmodifiableMap(); // Make sure we have an unmodifiable reference to the map below map=null; // Put this into the COPYONWRITE state (unless it is already frozen, in which case this is a noop) return createInCopyOnWriteState(unmodifiableMap()); } else { // This contains query profiles, don't try to defer copying return createInWritableState(deepClone(map)); } } private boolean isDeepUnmodifiable(Map map) { for (Object value : map.values()) if (value instanceof QueryProfile && !((QueryProfile)value).isFrozen()) return false; return true; // all other values are primitives } /** Deep clones a map - this handles all value types which can be found in a query profile */ static Map deepClone(Map map) { if (map==null) return null; Map mapClone=new HashMap<>(map.size()); for (Map.Entry entry : map.entrySet()) mapClone.put(entry.getKey(),QueryProfile.cloneIfNecessary(entry.getValue())); return mapClone; } //------- Content access ------------------------------------------------------- public Map unmodifiableMap() { if (isEmpty()) return Collections.emptyMap(); if (map==null) // in COPYONWRITE or FROZEN state return unmodifiableMap; // In WRITABLE state: Create unmodifiable wrapper if necessary and return it if (unmodifiableMap==null) unmodifiableMap=Collections.unmodifiableMap(map); return unmodifiableMap; } public Object get(String key) { if (map!=null) return map.get(key); if (unmodifiableMap!=null) return unmodifiableMap.get(key); return null; } public void put(String key,Object value) { ensureNotFrozen(); copyIfNotWritable(); if (map==null) map=new HashMap<>(); map.put(key,value); } public void remove(String key) { ensureNotFrozen(); copyIfNotWritable(); if (map!=null) map.remove(key); } private void copyIfNotWritable() { if (isWritable()) return; // move from COPYONWRITE to WRITABLE state map=new HashMap<>(unmodifiableMap); // deep clone is not necessary as this map is shallowly modifiable unmodifiableMap=null; // will be created as needed } }