From 95364f86f4cdfe787ba203d3dc346a10822054a5 Mon Sep 17 00:00:00 2001 From: Francisco Ferrari Bihurriet Date: Fri, 6 Dec 2024 05:56:26 +0100 Subject: [PATCH 1/7] 8345139: Fix bugs and inconsistencies in the Provider services map Co-authored-by: Francisco Ferrari Bihurriet Co-authored-by: Martin Balao --- .../share/classes/java/security/Provider.java | 1526 ++++++++++++----- .../Provider/ServicesConsistency.java | 1109 ++++++++++++ 2 files changed, 2247 insertions(+), 388 deletions(-) create mode 100644 test/jdk/java/security/Provider/ServicesConsistency.java diff --git a/src/java.base/share/classes/java/security/Provider.java b/src/java.base/share/classes/java/security/Provider.java index da3f53b963241..50f0de809c4bf 100644 --- a/src/java.base/share/classes/java/security/Provider.java +++ b/src/java.base/share/classes/java/security/Provider.java @@ -35,6 +35,7 @@ import static java.util.Locale.ENGLISH; import java.lang.ref.*; import java.lang.reflect.*; +import java.util.concurrent.atomic.AtomicReference; import java.util.function.BiConsumer; import java.util.function.BiFunction; import java.util.function.Function; @@ -196,8 +197,7 @@ protected Provider(String name, double version, String info) { this.version = version; this.versionStr = Double.toString(version); this.info = info; - this.serviceMap = new ConcurrentHashMap<>(); - this.legacyMap = new ConcurrentHashMap<>(); + this.servicesMap = new ServicesMap(); this.prngAlgos = new LinkedHashSet<>(6); putId(); initialized = true; @@ -237,8 +237,7 @@ protected Provider(String name, String versionStr, String info) { this.versionStr = versionStr; this.version = parseVersionStr(versionStr); this.info = info; - this.serviceMap = new ConcurrentHashMap<>(); - this.legacyMap = new ConcurrentHashMap<>(); + this.servicesMap = new ServicesMap(); this.prngAlgos = new LinkedHashSet<>(6); putId(); initialized = true; @@ -635,7 +634,7 @@ public synchronized Object merge(Object key, Object value, // let javadoc show doc from superclass @Override - public Object get(Object key) { + public synchronized Object get(Object key) { checkInitialized(); return super.get(key); } @@ -660,14 +659,14 @@ public synchronized void forEach(BiConsumer // let javadoc show doc from superclass @Override - public Enumeration keys() { + public synchronized Enumeration keys() { checkInitialized(); return super.keys(); } // let javadoc show doc from superclass @Override - public Enumeration elements() { + public synchronized Enumeration elements() { checkInitialized(); return super.elements(); } @@ -684,30 +683,844 @@ private void checkInitialized() { } } - // legacyMap changed since last call to getServices() - private transient volatile boolean legacyChanged; - // serviceMap changed since last call to getServices() - private transient volatile boolean servicesChanged; - - // Map - // used for services added via putService(), initialized on demand - private transient Map serviceMap; - // For backward compatibility, the registration ordering of // SecureRandom (RNG) algorithms needs to be preserved for // "new SecureRandom()" calls when this provider is used // NOTE: may need extra mechanism for providers to indicate their // preferred ordering of SecureRandom algorithms since registration // ordering info is lost once serialized - private transient Set prngAlgos; + private transient Set prngAlgos; + + // Map of services registered by this provider. This map may be republished + // (assigned to a new one instead of modified in place) when a series of + // changes with the Legacy API must be seen by readers as an atomic + // operation. See Providers::putAll. + private transient volatile ServicesMap servicesMap; + + /* + * This class defines a structure to store and obtain services registered by + * this provider, according to the Current (preferred) and Legacy APIs. + * Synchronization is required for write accesses, while reads are + * lock-free. + */ + private final class ServicesMap { + /* + * Enum to inform the result of an operation on the services map. + */ + enum SvcOpResult { + SUCCESS, + ERROR + } + + /* + * Interface to add and remove services to the map according to the + * Current API. These functions update the Properties map to reflect + * service changes, including algorithms, aliases and attributes. + * + * Services added with the Legacy API may be overwritten with this API. + * + * This interface guarantees atomicity from a service reader point + * of view. In other words, a reader that gets a service will see all + * its attributes and aliases as they were at time of registration. + */ + interface Current { + SvcOpResult putService(Service svc); + SvcOpResult removeService(Service svc); + } + + /* + * Interface to add, modify and remove services on the map according to + * the Legacy API. These functions update the Properties map to reflect + * service changes, including algorithms, aliases and attributes. + * + * Services added with the Current API cannot be overwritten with + * this API. + * + * Notice that this interface does not guarantee atomicity in a + * sequence of operations from a service reader point of view. As + * an example, a service reader may get a service missing an attribute + * if looked up between a writer's putClassName() and putAttribute() + * calls. For atomic changes with the Legacy API see Provider::putAll. + */ + interface Legacy { + SvcOpResult putClassName(ServiceKey key, String className, + String propKey); + SvcOpResult putAlias(ServiceKey key, ServiceKey aliasKey, + String propKey); + SvcOpResult putAttribute(ServiceKey key, String attrName, + String attrValue, String propKey); + SvcOpResult remove(ServiceKey key, String className); + SvcOpResult removeAlias(ServiceKey key, ServiceKey aliasKey); + SvcOpResult removeAttribute(ServiceKey key, String attrName, + String attrValue); + } + + /* + * This class is the internal implementation of the services map. + * Services can be added or removed either through the Current or the + * Legacy API. + */ + private final class ServicesMapImpl implements Current, Legacy { + /* + * Record to aggregate information about the lookup of a service on + * the internal map. See ServicesMapImpl::find for a description of + * possible values. + */ + private record MappingInfo(Service svc, ServiceKey algKey, + Boolean isLegacy) {} + + // The internal services map, containing services registered with + // the Current and the Legacy APIs. Concurrent read and write access + // to this map is expected. Both algorithm and alias service keys + // are added to this map. + private final Map services; + + // Auxiliary set to determine if a service on the services map + // was added with the Legacy API. The absence of a service key + // on this set is an indication that the service was either not + // added or added with the Current API. Only algorithm service keys + // are added to this set. + private final Set legacySvcKeys; + + // Auxiliary map to keep track of the Properties map entries that + // originated entries on the internal map. This information is used + // to avoid inconsistencies. Both algorithm and alias service keys + // are added to this map. + private final Map serviceProps; + + // Auxiliary map to keep track of the Properties map entries that + // originated service attributes on the internal map. This + // information is used to avoid inconsistencies. Only algorithm + // service keys are added to this map. + private final Map> + serviceAttrProps; + + ServicesMapImpl() { + services = new ConcurrentHashMap<>(); + legacySvcKeys = new HashSet<>(); + serviceProps = new HashMap<>(); + serviceAttrProps = new HashMap<>(); + } + + /* + * Constructor to create a thin working copy such that readers of + * the original map do not notice new changes. Used for atomic + * changes with the Legacy API. See Providers::putAll. + */ + ServicesMapImpl(ServicesMapImpl original) { + services = new ConcurrentHashMap<>(original.services); + legacySvcKeys = original.legacySvcKeys; + serviceProps = original.serviceProps; + serviceAttrProps = original.serviceAttrProps; + } + + /* + * Finds information about a service on the internal map. The key + * for the lookup can be either algorithm or alias based. If the + * service is found, svc refers to it, algKey to the algorithm + * service key and isLegacy informs if the service was stored with + * the Current or the Legacy API. Otherwise, svc is null, algKey + * refers to the key used for the lookup and isLegacy is null. + */ + private MappingInfo find(ServiceKey key) { + Service svc = services.get(key); + ServiceKey algKey = svc != null ? svc.algKey : key; + Boolean isLegacy = svc != null ? + legacySvcKeys.contains(algKey) : null; + return new MappingInfo(svc, algKey, isLegacy); + } + + /* + * Returns a set of services with services stored on the internal + * map. This method can be invoked concurrently with write accesses + * on the map and is lock-free. + */ + Set getServices() { + Set set = new LinkedHashSet<>(); + for (Map.Entry e : services.entrySet()) { + Service svc = e.getValue(); + // + // Skip alias based entries and filter out invalid services. + // + // Note: Multiple versions of the same service (reflecting + // different points in time) can be generated by concurrent + // writes with the Legacy API and, as a result of the + // copy-on-write strategy, seen under different service + // keys here. Each version has a unique object identity + // and, thus, would be distinguishable for a Set + // set. To avoid duplicates, we skip alias keys and use + // the version of the service pointed by the algorithm key. + if (e.getKey().equals(svc.algKey) && isValid(svc)) { + set.add(svc); + } + } + return set; + } + + Service getService(ServiceKey key) { + Service svc = services.get(key); + return svc != null && isValid(svc) ? svc : null; + } + + void clear() { + services.clear(); + legacySvcKeys.clear(); + serviceProps.clear(); + serviceAttrProps.clear(); + } + + /* + * Signals that there were changes on the services map and the + * cached set of services need to be recomputed before use. + */ + private void notifyChanges() { + serviceSet.set(null); + } + + /* + * A service is invalid if it was added with the Legacy API through + * an alias and does not have class information yet. We keep these + * services on the internal map but filter them out for readers, so + * they don't cause a NullPointerException when trying to create a + * new instance. + */ + private boolean isValid(Service svc) { + return svc.className != null; + } + + /* + * Current API methods to add and remove services. + */ + + @Override + public SvcOpResult putService(Service svc) { + svc.generateServiceKeys(); + + // Define a set of algorithm and alias keys that, if already + // on the services map, will be kept at all times until + // overwritten. This prevents concurrent readers from seeing + // 'holes' on the map while doing updates. + Set keysToBeKept = + new HashSet<>(svc.aliasKeys.size() + 1); + keysToBeKept.add(svc.algKey); + keysToBeKept.addAll(svc.aliasKeys.keySet()); + + // The new service algorithm key may be in use already. + resolveKeyConflict(svc.algKey, keysToBeKept); + + // The service will be registered to its provider's ServicesMap. + svc.registered = true; + + // Register the new service under its algorithm service key. + // At this point, readers will have access to it. + services.put(svc.algKey, svc); + + // Add an entry to the Properties map to reflect the new service + // under its algorithm key, and keep track of this information + // for further changes in the future (i.e. removal of the + // service). + String propKey = svc.getType() + "." + svc.getAlgorithm(); + serviceProps.put(svc.algKey, propKey); + Provider.super.put(propKey, svc.getClassName()); + + // Register the new service under its aliases. + for (Map.Entry e : + svc.aliasKeys.entrySet()) { + ServiceKey aliasKey = e.getKey(); + + // The new service alias may be in use already. + resolveKeyConflict(aliasKey, keysToBeKept); + + // Register the new service under its alias service key. At + // this point, readers will have access through this alias. + services.put(aliasKey, svc); + + // Add an entry to the Properties map to reflect the new + // service under its alias service key, and keep track + // of this information for further changes in the future + // (i.e. removal of the service). + propKey = ALIAS_PREFIX + svc.getType() + "." + e.getValue(); + serviceProps.put(aliasKey, propKey); + Provider.super.put(propKey, svc.getAlgorithm()); + } + + if (!svc.attributes.isEmpty()) { + // Register the new service attributes on the Properties map + // and keep track of them for further changes in the future + // (i.e. removal of the service). + Map newAttrProps = + new HashMap<>(svc.attributes.size()); + for (Map.Entry attr : + svc.attributes.entrySet()) { + propKey = svc.getType() + "." + svc.getAlgorithm() + + " " + attr.getKey().string; + newAttrProps.put(attr.getKey(), propKey); + Provider.super.put(propKey, attr.getValue()); + } + serviceAttrProps.put(svc.algKey, newAttrProps); + } + + Provider.this.checkAndUpdateSecureRandom(svc.algKey, true); + + return SvcOpResult.SUCCESS; + } + + /* + * Handle cases in which a service key (algorithm or alias based) + * is in use already. This might require modifications to a service, + * the Properties map or auxiliary structures. This method must be + * called from the Current API only. + */ + private void resolveKeyConflict(ServiceKey key, + Set keysToBeKept) { + assert keysToBeKept.contains(key) : "Inconsistent " + + "keysToBeKept set."; + MappingInfo miByKey = find(key); + if (miByKey.svc != null) { + // The service key (algorithm or alias) is in use already. + SvcOpResult opResult = SvcOpResult.SUCCESS; + if (miByKey.algKey.equals(key)) { + // It is used as an algorithm. Remove the service. + opResult = removeCommon(miByKey, false, keysToBeKept); + } else { + // It is used as an alias. + if (miByKey.isLegacy) { + // The service was added with the Legacy API. + // Remove the alias only. + opResult = removeAlias(miByKey.algKey, key, + keysToBeKept); + } else { + // The service was added with the Current API. + // Overwrite the alias entry on the services map + // without modifying the service that is currently + // using it. + + // Remove any Properties map key entry because, if + // no longer used as an alias, the entry would not + // be overwritten. Note: The serviceProps key entry + // will be overwritten later. + String oldPropKey = serviceProps.remove(key); + assert oldPropKey != null : + "Invalid alias property."; + Provider.super.remove(oldPropKey); + } + } + assert opResult == SvcOpResult.SUCCESS : "Unexpected" + + " error removing an existing service or alias."; + } + } + + @Override + public SvcOpResult removeService(Service svc) { + if (svc.algKey != null) { + MappingInfo mi = find(svc.algKey); + if (mi.svc != null) { + SvcOpResult opResult = removeCommon(mi, false, + Collections.emptySet()); + assert opResult == SvcOpResult.SUCCESS : "Unexpected" + + " error removing an existing service."; + return opResult; + } + } + return SvcOpResult.SUCCESS; + } + + /* + * Common (Current and Legacy) API methods to add and remove + * services. + */ + + /* + * This method is invoked both when removing and overwriting a + * service. The keysToBeKept set is used when overwriting to + * prevent readers from seeing a 'hole' on the services map + * between removing and adding entries. + */ + private SvcOpResult removeCommon(MappingInfo mi, + boolean legacyApiCall, Set keysToBeKept) { + assert mi.svc != null : "Invalid service for removal."; + if (!mi.isLegacy && legacyApiCall) { + // Services added with the Current API cannot be + // removed with the Legacy API. + return SvcOpResult.ERROR; + } + + if (mi.isLegacy) { + legacySvcKeys.remove(mi.algKey); + } + + if (!keysToBeKept.contains(mi.algKey)) { + services.remove(mi.algKey); + } + + // Update the Properties map to reflect the algorithm removal. + // Note: oldPropKey may be null for services added through + // aliases or attributes (Legacy API) that still don't have a + // class name (invalid). + String oldPropKey = serviceProps.remove(mi.algKey); + if (oldPropKey != null) { + Provider.super.remove(oldPropKey); + } + + // Remove registered service aliases. + for (ServiceKey aliasKey : mi.svc.aliasKeys.keySet()) { + if (!mi.isLegacy) { + // Services added with the Current API can have aliases + // overwritten by other services added with the same + // API. Do nothing in these cases: the alias on the + // services map does not belong to the removed service + // anymore. + MappingInfo miByAlias = find(aliasKey); + if (miByAlias.svc != mi.svc) { + continue; + } + } + if (!keysToBeKept.contains(aliasKey)) { + services.remove(aliasKey); + } + + // Update the Properties map to reflect the alias removal. + // Note: oldPropKey cannot be null because aliases always + // have a corresponding Properties map entry. + oldPropKey = serviceProps.remove(aliasKey); + assert oldPropKey != null : "Unexpected null " + + "Property value for an alias."; + Provider.super.remove(oldPropKey); + } + + // Remove registered service attributes. + Map oldAttrProps = + serviceAttrProps.remove(mi.algKey); + if (oldAttrProps != null) { + for (String oldAttrPropKey : oldAttrProps.values()) { + // Update the Properties map to reflect the attribute + // removal. Note: oldAttrPropKey cannot be null because + // attributes always have a corresponding Properties map + // entry. + assert oldAttrPropKey != null : "Unexpected null " + + "Property value for an attribute."; + Provider.super.remove(oldAttrPropKey); + } + } + + notifyChanges(); + + Provider.this.checkAndUpdateSecureRandom(mi.svc.algKey, false); + + return SvcOpResult.SUCCESS; + } - // Map - // used for services added via legacy methods, init on demand - private transient Map legacyMap; + /* + * Legacy API methods to add, modify and remove services. + */ + + @Override + public SvcOpResult putClassName(ServiceKey key, String className, + String propKey) { + assert key != null && className != null && propKey != null : + "Service information missing."; + return updateSvc(key, (MappingInfo oldMi, Service newSvc) -> { + String canonicalPropKey = propKey; + if (oldMi.svc != null) { + // The service exists. Get its Properties map entry. + // Note: Services added through an alias or an attribute + // may don't have one. + String oldPropKey = serviceProps.get(oldMi.algKey); + if (oldMi.algKey.equals(key)) { + // The service was found by an algorithm. + if (oldPropKey != null) { + // Remove any previous Properties map entry + // before adding a new one, so we handle + // differences in casing. + Provider.super.remove(oldPropKey); + } + } else { + // The service was found by an alias. Use an + // algorithm entry on the Properties map. Create a + // new one if it does not exist. + canonicalPropKey = oldPropKey != null ? + oldPropKey : newSvc.getType() + "." + + newSvc.getAlgorithm(); + } + } + + newSvc.className = className; + + // Keep track of the Properties map entry for further + // changes in the future (i.e. removal of the service). + serviceProps.put(oldMi.algKey, canonicalPropKey); + Provider.super.put(canonicalPropKey, className); + + Provider.this.checkAndUpdateSecureRandom( + newSvc.algKey, true); + + return SvcOpResult.SUCCESS; + }); + } + + @Override + public SvcOpResult putAlias(ServiceKey key, ServiceKey aliasKey, + String propKey) { + assert key != null && aliasKey != null && propKey != null : + "Alias information missing."; + assert key.type.equals(aliasKey.type) : + "Inconsistent service key types."; + return updateSvc(key, (MappingInfo oldMi, Service newSvc) -> { + MappingInfo miByAlias = find(aliasKey); + if (miByAlias.svc != null) { + // The alias is associated to a service on the map. + if (miByAlias.algKey.equals(aliasKey)) { + // The alias is an algorithm. Never overwrite + // algorithms with aliases from the Legacy API. + return SvcOpResult.ERROR; + } else if (!miByAlias.isLegacy) { + // Do not remove the alias of services added with + // the Current API. + return SvcOpResult.ERROR; + } else if (miByAlias.svc == oldMi.svc) { + // The service has the alias that we are adding. + // This is possible if, for example, the alias + // casing is changing. + // + // Update the Properties map to remove the alias + // with the old casing. Note: oldPropKey cannot be + // null because aliases always have a corresponding + // Properties map entry. + String oldPropKey = serviceProps.remove(aliasKey); + assert oldPropKey != null : "Unexpected null " + + "Property value for an alias."; + Provider.super.remove(oldPropKey); + } else { + // The alias belongs to a different service. + // Remove it first. + SvcOpResult opResult = removeAlias(miByAlias.algKey, + aliasKey, Set.of(aliasKey)); + assert opResult == SvcOpResult.SUCCESS : + "Unexpected error removing an alias."; + } + } else { + // The alias was not found on the map. + if (aliasKey.equals(key)) { + // The alias would be equal to the algorithm for + // the new service. + return SvcOpResult.ERROR; + } + } + + newSvc.addAliasKey(aliasKey); + + // Keep track of the Properties map entry for further + // changes in the future (i.e. removal of the service). + serviceProps.put(aliasKey, propKey); + // If the service to which we will add an alias was found by + // an alias, use its algorithm for the Properties map entry. + String canonicalAlgorithm = oldMi.algKey.equals(key) ? + key.originalAlgorithm : newSvc.getAlgorithm(); + Provider.super.put(propKey, canonicalAlgorithm); + + return SvcOpResult.SUCCESS; + }); + } + + @Override + public SvcOpResult putAttribute(ServiceKey key, String attrName, + String attrValue, String propKey) { + assert key != null && attrName != null && attrValue != null && + propKey != null : "Attribute information missing."; + return updateSvc(key, (MappingInfo oldMi, Service newSvc) -> { + String canonicalPropKey = propKey; + UString attrNameKey = new UString(attrName); + Map attrProps = + serviceAttrProps.computeIfAbsent( + oldMi.algKey, k -> new HashMap<>()); + assert oldMi.svc != null || attrProps.isEmpty() : + "Inconsistent service attributes data."; + // Try to get the attribute's Properties map entry. Note: + // oldPropKey can be null if the service was not found or + // does not have the attribute. + String oldPropKey = attrProps.get(attrNameKey); + if (oldMi.algKey.equals(key)) { + // The service was found by an algorithm. + if (oldPropKey != null) { + // Remove any previous Properties map entry before + // adding a new one, so we handle differences in + // casing. + Provider.super.remove(oldPropKey); + } + } else { + // The service was found by an alias. Use an algorithm + // based entry on the Properties map. Create a new one + // if it does not exist. + canonicalPropKey = oldPropKey != null ? oldPropKey : + newSvc.getType() + "." + newSvc.getAlgorithm() + + " " + attrName; + } + + newSvc.addAttribute(attrName, attrValue); + + // Keep track of the Properties map entry for further + // changes in the future (i.e. removal of the service). + attrProps.put(attrNameKey, canonicalPropKey); + Provider.super.put(canonicalPropKey, attrValue); + + return SvcOpResult.SUCCESS; + }); + } + + @Override + public SvcOpResult remove(ServiceKey key, String className) { + assert key != null && className != null : + "Service information missing."; + MappingInfo mi = find(key); + if (mi.svc != null) { + assert className.equals(mi.svc.getClassName()) : + "Unexpected class name."; + return removeCommon(mi, true, Collections.emptySet()); + } + assert false : "Should not reach."; + return SvcOpResult.ERROR; + } + + @Override + public SvcOpResult removeAlias(ServiceKey key, + ServiceKey aliasKey) { + return removeAlias(key, aliasKey, Collections.emptySet()); + } + + /* + * This method is invoked both when removing and overwriting a + * service alias. The keysToBeKept set is used when overwriting to + * prevent readers from seeing a 'hole' on the services map between + * removing and adding entries. + */ + private SvcOpResult removeAlias(ServiceKey key, ServiceKey aliasKey, + Set keysToBeKept) { + assert key != null && aliasKey != null && keysToBeKept != null : + "Alias information missing."; + return updateSvc(key, (MappingInfo oldMi, Service newSvc) -> { + MappingInfo miByAlias = find(aliasKey); + if (oldMi.svc != null && miByAlias.svc == oldMi.svc && + !miByAlias.algKey.equals(aliasKey)) { + // The alias is a real alias and is associated to the + // service on the map. + if (!keysToBeKept.contains(aliasKey)) { + services.remove(aliasKey); + } - // Set - // Unmodifiable set of all services. Initialized on demand. - private transient volatile Set serviceSet; + newSvc.removeAliasKey(aliasKey); + + // Update the Properties map to reflect the alias + // removal. Note: oldPropKey cannot be null because + // aliases always have a corresponding Properties map + // entry. + String oldPropKey = serviceProps.remove(aliasKey); + assert oldPropKey != null : "Invalid alias property."; + Provider.super.remove(oldPropKey); + + return SvcOpResult.SUCCESS; + } + assert false : "Should not reach."; + return SvcOpResult.ERROR; + }); + } + + @Override + public SvcOpResult removeAttribute(ServiceKey key, + String attrName, String attrValue) { + assert key != null && attrName != null && attrValue != null : + "Attribute information missing."; + return updateSvc(key, (MappingInfo oldMi, Service newSvc) -> { + Map oldAttrProps = + serviceAttrProps.get(oldMi.algKey); + if (oldAttrProps != null) { + // The service was found and has attributes. + assert oldMi.svc != null : "Inconsistent service " + + "attributes data."; + + newSvc.removeAttribute(attrName, attrValue); + assert newSvc.getAttribute(attrName) == null : + "Attribute was not removed from the service."; + + // Update the Properties map to reflect the attribute + // removal. Note: oldPropKey cannot be null because + // attributes always have a corresponding Properties + // map entry. + String oldPropKey = oldAttrProps.remove( + new UString(attrName)); + assert oldPropKey != null : + "Invalid attribute property."; + Provider.super.remove(oldPropKey); + + if (oldAttrProps.isEmpty()) { + // If the removed attribute was the last one, + // remove the map. + serviceAttrProps.remove(oldMi.algKey); + } + + return SvcOpResult.SUCCESS; + } + assert false : "Should not reach."; + return SvcOpResult.ERROR; + }); + } + + @FunctionalInterface + private interface ServiceUpdateCallback { + SvcOpResult apply(MappingInfo oldMi, Service newSvc); + } + + /* + * This method tries to find a service on the map (based on an + * algorithm or alias) and pass a copy of it to an update callback + * (copy-on-write). If the service found was added with the Current + * API, no update should be done. If a service was not found, a new + * instance may be created. + * + * The updated version of the service is put on the services map. + * Algorithm and alias based entries pointing to the old version + * of the service are overwritten. + */ + private SvcOpResult updateSvc(ServiceKey key, + ServiceUpdateCallback updateCb) { + Service newSvc; + MappingInfo oldMi = find(key); + if (oldMi.svc != null) { + // Service exists. + if (!oldMi.isLegacy) { + // Don't update services added with the Current API. + return SvcOpResult.ERROR; + } + // Create a copy of the service for a copy-on-write update. + newSvc = new Service(oldMi.svc); + } else { + // Service does not exist. + newSvc = new Service(Provider.this, key); + } + SvcOpResult opResult = updateCb.apply(oldMi, newSvc); + if (opResult == SvcOpResult.ERROR) { + // Something went wrong and the update should not be done. + return opResult; + } + + // The service (or its updated version) will be registered to + // its provider's ServicesMap. + newSvc.registered = true; + + // Register the updated version of the service under its + // algorithm and aliases on the map. This may overwrite entries + // or add new ones. The previous callback should have handled + // the removal of an alias. + for (ServiceKey aliasKey : newSvc.aliasKeys.keySet()) { + services.put(aliasKey, newSvc); + } + + assert oldMi.algKey.type.equals(newSvc.getType()) && + oldMi.algKey.originalAlgorithm.equals( + newSvc.getAlgorithm()) : "Invalid key."; + services.put(oldMi.algKey, newSvc); + + legacySvcKeys.add(oldMi.algKey); + + // Notify a change. + notifyChanges(); + + return opResult; + } + } + + // Placeholder for a thread to mark that serviceSet values are being + // computed after a services update. Only one thread at a time can + // effectively assign this value. + private static final Set SERVICE_SET_IN_PROGRESS = Set.of(); + + // Unmodifiable set of all services. Possible values for this field + // are: 1) null (indicates that the set has to be recomputed after a + // service update), 2) SERVICE_SET_IN_PROGRESS (indicates that a thread + // is recomputing its value), and 3) an actual set of services. + private final AtomicReference> serviceSet; + + // Implementation of ServicesMap that handles the Current and Legacy + // APIs. + private final ServicesMapImpl impl; + + ServicesMap() { + impl = new ServicesMapImpl(); + serviceSet = new AtomicReference<>(); + } + + /* + * Constructor to create a thin working copy such that readers of the + * original map do not notice any new changes. Used for atomic + * changes with the Legacy API. See Providers::putAll. + */ + ServicesMap(ServicesMap original) { + impl = new ServicesMapImpl(original.impl); + serviceSet = new AtomicReference<>(original.serviceSet.get()); + } + + /* + * Returns a Current API view of the services map. + */ + Current asCurrent() { + return impl; + } + + /* + * Returns a Legacy API view of the services map. + */ + Legacy asLegacy() { + return impl; + } + + /* + * Returns an unmodifiable set of available services. Recomputes + * serviceSet if needed, after a service update. Both services added + * with the Current and Legacy APIs are included. If no services are + * found, the returned set is empty. This method is thread-safe and + * lock-free. + */ + Set getServices() { + Set serviceSetLocal = serviceSet.compareAndExchange( + null, SERVICE_SET_IN_PROGRESS); + if (serviceSetLocal == null || + serviceSetLocal == SERVICE_SET_IN_PROGRESS) { + // A cached set is not available. Instead of locking, compute + // the set to be returned and, eventually, make it available + // for others to use. + Set newSet = Collections.unmodifiableSet( + impl.getServices()); + if (serviceSetLocal == null) { + // We won the race to make the computed set available for + // others to use. However, only make it available if it + // is still current (in other words, there were no further + // changes). If it is not current, the next reader will + // do the job. + serviceSet.compareAndExchange( + SERVICE_SET_IN_PROGRESS, newSet); + } + serviceSetLocal = newSet; + } + return serviceSetLocal; + } + + /* + * Returns an available service. Both services added with the Current + * and Legacy APIs are considered in the search. Thread-safe and + * lock-free. + */ + Service getService(ServiceKey key) { + return impl.getService(key); + } + + /* + * Clears the internal ServicesMap state. The caller must synchronize + * changes with the Properties map. + */ + void clear() { + impl.clear(); + serviceSet.set(null); + } + } // register the id attributes for this provider // this is to ensure that equals() and hashCode() do not incorrectly @@ -720,6 +1533,19 @@ private void putId() { super.put("Provider.id className", this.getClass().getName()); } + /* + * Creates a copy of the Properties map that is useful to iterate when + * applying changes to the original one. Notice that we are calling + * super.entrySet() purposefully to avoid landing into a subclass override. + */ + private Properties copyProperties() { + Properties copy = new Properties(super.size()); + for (Map.Entry entry : super.entrySet()) { + copy.put(entry.getKey(), entry.getValue()); + } + return copy; + } + /** * Reads the {@code ObjectInputStream} for the default serializable fields. * If the serialized field {@code versionStr} is found in the STREAM FIELDS, @@ -735,11 +1561,7 @@ private void putId() { @java.io.Serial private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException { - Map copy = new HashMap<>(); - for (Map.Entry entry : super.entrySet()) { - copy.put(entry.getKey(), entry.getValue()); - } - + Properties copy = copyProperties(); defaults = null; in.defaultReadObject(); if (this.versionStr == null) { @@ -749,196 +1571,195 @@ private void readObject(ObjectInputStream in) // otherwise, set version based on versionStr this.version = parseVersionStr(this.versionStr); } - this.serviceMap = new ConcurrentHashMap<>(); - this.legacyMap = new ConcurrentHashMap<>(); + this.servicesMap = new ServicesMap(); this.prngAlgos = new LinkedHashSet<>(6); implClear(); initialized = true; putAll(copy); } - // returns false if no update necessary, i.e. key isn't String or - // is String, but it's provider-related (name/version/info/className) - private static boolean checkLegacy(Object key) { - if (key instanceof String && ((String)key).startsWith("Provider.")) { - // ignore provider related updates - return false; - } else { - return true; + /* + * Enum to determine if changes to the Properties map must be applied by the + * caller (UPDATE) or skipped (SKIP). + * + * If a change does not concern a ServicesMap, UPDATE is returned. An + * example of this is when adding, modifying or removing an entry that is + * not a service, alias or attribute. + * + * If the change concerns a ServicesMap, SKIP is returned. The change may + * have been applied internally or ignored due to an error. In the former + * case, Properties map entries are synchronized. In the latter, Properties + * map entries are not modified. + */ + private enum PropertiesMapAction { + UPDATE, + SKIP + } + + private PropertiesMapAction doLegacyOp(ServicesMap servicesMap, Object key, + Object value, Object oldValue, OPType opType) { + if (key instanceof String ks) { + if (ks.startsWith("Provider.")) { + // Ignore provider related updates. + return PropertiesMapAction.SKIP; + } + if (value instanceof String vs) { + return parseLegacy(servicesMap, ks, vs, opType); + } else if (value != null && oldValue instanceof String oldValueS && + opType == OPType.ADD) { + // An entry in the Properties map potentially concerning the + // ServicesMap is about to be replaced by one that does not. + // From the ServicesMap point of view, this could be equivalent + // to a removal. In any case, let the caller proceed with the + // Properties map update. + parseLegacy(servicesMap, ks, oldValueS, OPType.REMOVE); + } } + // The change does not concern a ServicesMap. + return PropertiesMapAction.UPDATE; } /** * Copies all the mappings from the specified Map to this provider. */ private void implPutAll(Map t) { + // For service readers to see this change as atomic, add the elements in + // a local thin copy of the ServicesMap and then publish it. + ServicesMap servicesMapCopy = new ServicesMap(servicesMap); for (Map.Entry e : t.entrySet()) { - implPut(e.getKey(), e.getValue()); + implPut(servicesMapCopy, e.getKey(), e.getValue()); } + servicesMap = servicesMapCopy; } - private Object implRemove(Object key) { - if (!checkLegacy(key)) return null; + private void implClear() { + servicesMap.clear(); + prngAlgos.clear(); + super.clear(); + putId(); + } - Object o = super.remove(key); - if (o instanceof String so && key instanceof String sk) { - parseLegacy(sk, so, OPType.REMOVE); - } - return o; + private Object implRemove(Object key) { + Object oldValue = super.get(key); + return doLegacyOp(servicesMap, key, oldValue, null, OPType.REMOVE) == + PropertiesMapAction.UPDATE ? super.remove(key) : oldValue; } private boolean implRemove(Object key, Object value) { - if (!checkLegacy(key)) return false; - - boolean result = super.remove(key, value); - if (result && key instanceof String sk && value instanceof String sv) { - parseLegacy(sk, sv, OPType.REMOVE); + if (Objects.equals(super.get(key), value) && value != null) { + implRemove(key); + return !super.contains(key); } - return result; + return false; } private boolean implReplace(Object key, Object oldValue, Object newValue) { - if (!checkLegacy(key)) return false; - - boolean result = super.replace(key, oldValue, newValue); - if (result && key instanceof String sk) { - if (newValue instanceof String sv) { - parseLegacy(sk, sv, OPType.ADD); - } else if (oldValue instanceof String sv) { - parseLegacy(sk, sv, OPType.REMOVE); - } + Objects.requireNonNull(oldValue); + Objects.requireNonNull(newValue); + if (super.containsKey(key) && + Objects.equals(super.get(key), oldValue)) { + implPut(key, newValue); + return super.get(key) == newValue; } - return result; + return false; } private Object implReplace(Object key, Object value) { - if (!checkLegacy(key)) return null; - - Object o = super.replace(key, value); - if (key instanceof String sk) { - if (o instanceof String so) { - if (value instanceof String sv) { - parseLegacy(sk, sv, OPType.ADD); - } else { - parseLegacy(sk, so, OPType.REMOVE); - } - } + Objects.requireNonNull(value); + if (super.containsKey(key)) { + return implPut(key, value); } - return o; + return null; } @SuppressWarnings("unchecked") // Function must actually operate over strings private void implReplaceAll(BiFunction function) { - - super.replaceAll(function); - // clear out all existing mappings and start fresh - legacyMap.clear(); - legacyChanged = true; - for (Map.Entry entry : super.entrySet()) { - Object key = entry.getKey(); - Object value = entry.getValue(); - if ((key instanceof String sk) && (value instanceof String sv)) { - if (!checkLegacy(sk)) { - continue; - } - parseLegacy(sk, sv, OPType.ADD); - } - } + Properties propertiesCopy = copyProperties(); + propertiesCopy.replaceAll(function); + putAll(propertiesCopy); } @SuppressWarnings("unchecked") // Function must actually operate over strings private Object implMerge(Object key, Object value, BiFunction remappingFunction) { - if (!checkLegacy(key)) return null; - - Object o = super.merge(key, value, remappingFunction); - if (key instanceof String sk) { - if (o == null) { - parseLegacy(sk, null, OPType.REMOVE); - } else if (o instanceof String so) { - parseLegacy(sk, so, OPType.ADD); - } + Objects.requireNonNull(value); + Object oldValue = super.get(key); + Object newValue = (oldValue == null) ? value : + remappingFunction.apply(oldValue, value); + if (newValue == null) { + implRemove(key); + } else { + implPut(key, newValue); } - return o; + return super.get(key); } @SuppressWarnings("unchecked") // Function must actually operate over strings private Object implCompute(Object key, BiFunction remappingFunction) { - - if (!checkLegacy(key)) return null; - - Object o = super.compute(key, remappingFunction); - if (key instanceof String sk) { - if (o == null) { - parseLegacy(sk, null, OPType.REMOVE); - } else if (o instanceof String so) { - parseLegacy(sk, so, OPType.ADD); - } + Object oldValue = super.get(key); + Object newValue = remappingFunction.apply(key, oldValue); + if (newValue != null) { + implPut(key, newValue); + } else if (oldValue != null) { + // The Properties map cannot contain null values, so checking + // super.containsKey(key) would be superfluous. + implRemove(key); } - return o; + return super.get(key); } @SuppressWarnings("unchecked") // Function must actually operate over strings private Object implComputeIfAbsent(Object key, Function mappingFunction) { - if (!checkLegacy(key)) return null; - - Object o = super.computeIfAbsent(key, mappingFunction); - if (o instanceof String so && key instanceof String sk) { - parseLegacy(sk, so, OPType.ADD); + Object oldValue = super.get(key); + if (oldValue == null) { + Object newValue = mappingFunction.apply(key); + if (newValue != null) { + implPut(key, newValue); + return super.get(key); + } } - return o; + return oldValue; } @SuppressWarnings("unchecked") // Function must actually operate over strings private Object implComputeIfPresent(Object key, BiFunction remappingFunction) { - if (!checkLegacy(key)) return null; - - Object o = super.computeIfPresent(key, remappingFunction); - if (o instanceof String so && key instanceof String sk) { - parseLegacy(sk, so, OPType.ADD); - } - return o; - } - - private Object implPut(Object key, Object value) { - if (!checkLegacy(key)) return null; - - Object o = super.put(key, value); - if (key instanceof String sk && value instanceof String sv) { - parseLegacy(sk, sv, OPType.ADD); + Object oldValue = super.get(key); + if (oldValue != null) { + Object newValue = remappingFunction.apply(key, oldValue); + if (newValue != null) { + implPut(key, newValue); + } else { + implRemove(key); + } + return super.get(key); } - return o; + return null; } private Object implPutIfAbsent(Object key, Object value) { - if (!checkLegacy(key)) return null; + Objects.requireNonNull(value); + Object oldValue = super.get(key); + return oldValue == null ? implPut(key, value) : oldValue; + } - Object o = super.putIfAbsent(key, value); - if (o == null && key instanceof String sk && - value instanceof String sv) { - parseLegacy(sk, sv, OPType.ADD); - } - return o; + private Object implPut(Object key, Object value) { + return implPut(servicesMap, key, value); } - private void implClear() { - legacyMap.clear(); - serviceMap.clear(); - legacyChanged = false; - servicesChanged = false; - serviceSet = null; - prngAlgos.clear(); - super.clear(); - putId(); + private Object implPut(ServicesMap servicesMap, Object key, Object value) { + Objects.requireNonNull(value); + Object oldValue = super.get(key); + return doLegacyOp(servicesMap, key, value, oldValue, OPType.ADD) == + PropertiesMapAction.UPDATE ? super.put(key, value) : oldValue; } // used as key in the serviceMap and legacyMap HashMaps - private static class ServiceKey { + private static final class ServiceKey { private final String type; private final String algorithm; private final String originalAlgorithm; @@ -992,134 +1813,79 @@ private enum OPType { ADD, REMOVE } - private void parseLegacy(String name, String value, OPType opType) { + /* + * Parse a String entry change on the Properties map and, if concerns to the + * ServicesMap, apply its corresponding operation through the Legacy API. + * Returns whether the change on the Properties map should proceed or not. + */ + private PropertiesMapAction parseLegacy(ServicesMap servicesMap, + String propKey, String propValue, OPType opType) { // alias - if (name.toLowerCase(ENGLISH).startsWith(ALIAS_PREFIX_LOWER)) { + if (propKey.toLowerCase(ENGLISH).startsWith(ALIAS_PREFIX_LOWER)) { // e.g. put("Alg.Alias.MessageDigest.SHA", "SHA-1"); // aliasKey ~ MessageDigest.SHA - String aliasKeyStr = name.substring(ALIAS_LENGTH); + String aliasKeyStr = propKey.substring(ALIAS_LENGTH); String[] typeAndAlg = getTypeAndAlgorithm(aliasKeyStr); if (typeAndAlg == null) { - return; + return PropertiesMapAction.UPDATE; } - legacyChanged = true; - Objects.requireNonNull(value, "alias value should map to an alg"); + Objects.requireNonNull(propValue, + "alias value should map to an alg"); String type = getEngineName(typeAndAlg[0]); String aliasAlg = typeAndAlg[1].intern(); - ServiceKey stdKey = new ServiceKey(type, value, true); - Service stdService = legacyMap.get(stdKey); + ServiceKey svcKey = new ServiceKey(type, propValue, true); ServiceKey aliasKey = new ServiceKey(type, aliasAlg, true); switch (opType) { - case ADD: - // clean up old alias if present - Service prevAliasService = legacyMap.get(aliasKey); - if (prevAliasService != null) { - prevAliasService.removeAlias(aliasAlg); - } - if (stdService == null) { - // add standard mapping in order to add alias - stdService = new Service(this, type, value); - legacyMap.put(stdKey, stdService); - } - stdService.addAlias(aliasAlg); - legacyMap.put(aliasKey, stdService); - break; - case REMOVE: - if (stdService != null) { - stdService.removeAlias(aliasAlg); - } - legacyMap.remove(aliasKey); - break; - default: - throw new AssertionError(); + case ADD -> servicesMap.asLegacy() + .putAlias(svcKey, aliasKey, propKey); + case REMOVE -> servicesMap.asLegacy() + .removeAlias(svcKey, aliasKey); } } else { - String[] typeAndAlg = getTypeAndAlgorithm(name); + String[] typeAndAlg = getTypeAndAlgorithm(propKey); if (typeAndAlg == null) { - return; + return PropertiesMapAction.UPDATE; } - legacyChanged = true; int i = typeAndAlg[1].indexOf(' '); // regular registration if (i == -1) { // e.g. put("MessageDigest.SHA-1", "sun.security.provider.SHA"); String type = getEngineName(typeAndAlg[0]); - String stdAlg = typeAndAlg[1].intern(); - ServiceKey stdKey = new ServiceKey(type, stdAlg, true); - Service stdService = legacyMap.get(stdKey); + String algo = typeAndAlg[1].intern(); + ServiceKey svcKey = new ServiceKey(type, algo, true); switch (opType) { - case ADD: - Objects.requireNonNull(value, - "className can't be null"); - if (stdService == null) { - stdService = new Service(this, type, stdAlg); - legacyMap.put(stdKey, stdService); - } - stdService.className = value; - break; - case REMOVE: - // only remove if value also matches when non-null - if (stdService != null) { - if (value == null) { - legacyMap.remove(stdKey); - } else if (stdService.className.equals(value)) { - legacyMap.remove(stdKey, stdService); - } - // remove all corresponding alias mappings - for (String alias : stdService.getAliases()) { - legacyMap.remove(new ServiceKey(type, alias, - true), stdService); - } - } - break; - default: - throw new AssertionError(); + case ADD -> servicesMap.asLegacy() + .putClassName(svcKey, propValue, propKey); + case REMOVE -> servicesMap.asLegacy() + .remove(svcKey, propValue); } - checkAndUpdateSecureRandom(type, stdAlg, - (opType != OPType.REMOVE)); } else { // attribute // e.g. put("MessageDigest.SHA-1 ImplementedIn", "Software"); String type = getEngineName(typeAndAlg[0]); String attrString = typeAndAlg[1]; - String stdAlg = attrString.substring(0, i).intern(); + String algo = attrString.substring(0, i).intern(); String attrName = attrString.substring(i + 1); // kill additional spaces while (attrName.startsWith(" ")) { attrName = attrName.substring(1); } attrName = attrName.intern(); - ServiceKey stdKey = new ServiceKey(type, stdAlg, true); - Service stdService = legacyMap.get(stdKey); + ServiceKey svcKey = new ServiceKey(type, algo, true); switch (opType) { - case ADD: - Objects.requireNonNull(value, - "attribute value should not be null"); - - if (stdService == null) { - stdService = new Service(this, type, stdAlg); - legacyMap.put(stdKey, stdService); - } - stdService.addAttribute(attrName, value); - break; - case REMOVE: - if (stdService != null) { - stdService.removeAttribute(attrName, value); - } - break; - default: - throw new AssertionError(); + case ADD -> servicesMap.asLegacy() + .putAttribute(svcKey, attrName, propValue, propKey); + case REMOVE -> servicesMap.asLegacy() + .removeAttribute(svcKey, attrName, propValue); } } } + return PropertiesMapAction.SKIP; } /** * Get the service describing this Provider's implementation of the * specified type of this algorithm or alias. If no such - * implementation exists, this method returns {@code null}. If there are two - * matching services, one added to this provider using - * {@link #putService putService()} and one added via {@link #put put()}, - * the service added via {@link #putService putService()} is returned. + * implementation exists, this method returns {@code null}. * * @param type the type of {@link Service service} requested * (for example, {@code MessageDigest}) @@ -1142,13 +1908,7 @@ public Service getService(String type, String algorithm) { previousKey.set(key); } - Service s = serviceMap.get(key); - if (s == null) { - s = legacyMap.get(key); - if (s != null && !s.isValid()) { - legacyMap.remove(key, s); - } - } + Service s = servicesMap.getService(key); if (s != null && SecurityProviderServiceEvent.isTurnedOn()) { var e = new SecurityProviderServiceEvent(); @@ -1181,34 +1941,15 @@ public Service getService(String type, String algorithm) { */ public Set getServices() { checkInitialized(); - if (serviceSet == null || legacyChanged || servicesChanged) { - Set set = new LinkedHashSet<>(); - if (!serviceMap.isEmpty()) { - set.addAll(serviceMap.values()); - } - if (!legacyMap.isEmpty()) { - legacyMap.entrySet().forEach(entry -> { - if (!entry.getValue().isValid()) { - legacyMap.remove(entry.getKey(), entry.getValue()); - } else { - set.add(entry.getValue()); - } - }); - } - serviceSet = Collections.unmodifiableSet(set); - servicesChanged = false; - legacyChanged = false; - } - return serviceSet; + return servicesMap.getServices(); } /** * Add a service. If a service of the same type with the same algorithm - * name exists, and it was added using {@link #putService putService()}, - * it is replaced by the new service. - * This method also places information about this service - * in the provider's Hashtable values in the format described in the - * {@extLink security_guide_jca + * name exists, and it was added using {@link #putService putService()} or + * {@link #put put()}, it is replaced by the new service. This method also + * places information about this service in the provider's Hashtable + * values in the format described in the {@extLink security_guide_jca * Java Cryptography Architecture (JCA) Reference Guide}. * * @param s the Service to add @@ -1217,7 +1958,7 @@ public Set getServices() { * * @since 1.5 */ - protected void putService(Service s) { + protected synchronized void putService(Service s) { checkInitialized(); if (debug != null) { debug.println(name + ".putService(): " + s); @@ -1229,32 +1970,18 @@ protected void putService(Service s) { throw new IllegalArgumentException ("service.getProvider() must match this Provider object"); } - String type = s.getType(); - String algorithm = s.getAlgorithm(); - ServiceKey key = new ServiceKey(type, algorithm, true); - implRemoveService(serviceMap.get(key)); - serviceMap.put(key, s); - for (String alias : s.getAliases()) { - serviceMap.put(new ServiceKey(type, alias, true), s); - } - servicesChanged = true; - synchronized (this) { - putPropertyStrings(s); - checkAndUpdateSecureRandom(type, algorithm, true); - } + servicesMap.asCurrent().putService(s); } - private void checkAndUpdateSecureRandom(String type, String algo, - boolean doAdd) { - if (type.equalsIgnoreCase("SecureRandom")) { + private void checkAndUpdateSecureRandom(ServiceKey algKey, boolean doAdd) { + if (algKey.type.equalsIgnoreCase("SecureRandom")) { if (doAdd) { - prngAlgos.add(algo); + prngAlgos.add(algKey); } else { - prngAlgos.remove(algo); + prngAlgos.remove(algKey); } if (debug != null) { - debug.println((doAdd? "Add":"Remove") + - " SecureRandom algo " + algo); + debug.println((doAdd ? "Add" : "Remove") + " " + algKey); } } } @@ -1265,7 +1992,7 @@ Service getDefaultSecureRandomService() { checkInitialized(); if (!prngAlgos.isEmpty()) { - String algo = prngAlgos.iterator().next(); + String algo = prngAlgos.iterator().next().originalAlgorithm; // IMPORTANT: use the Service obj returned by getService(...) call // as providers may override putService(...)/getService(...) and // return their own Service objects @@ -1276,44 +2003,8 @@ Service getDefaultSecureRandomService() { } /** - * Put the string properties for this Service in this Provider's - * Hashtable. - */ - private void putPropertyStrings(Service s) { - String type = s.getType(); - String algorithm = s.getAlgorithm(); - // use super() to avoid other processing - super.put(type + "." + algorithm, s.getClassName()); - for (String alias : s.getAliases()) { - super.put(ALIAS_PREFIX + type + "." + alias, algorithm); - } - for (Map.Entry entry : s.attributes.entrySet()) { - String key = type + "." + algorithm + " " + entry.getKey(); - super.put(key, entry.getValue()); - } - } - - /** - * Remove the string properties for this Service from this Provider's - * Hashtable. - */ - private void removePropertyStrings(Service s) { - String type = s.getType(); - String algorithm = s.getAlgorithm(); - // use super() to avoid other processing - super.remove(type + "." + algorithm); - for (String alias : s.getAliases()) { - super.remove(ALIAS_PREFIX + type + "." + alias); - } - for (Map.Entry entry : s.attributes.entrySet()) { - String key = type + "." + algorithm + " " + entry.getKey(); - super.remove(key); - } - } - - /** - * Remove a service previously added using - * {@link #putService putService()}. The specified service is removed from + * Remove a service previously added using {@link #putService putService()} + * or {@link #put put()}. The specified service is removed from * this {@code Provider}. It will no longer be returned by * {@link #getService getService()} and its information will be removed * from this provider's Hashtable. @@ -1324,7 +2015,7 @@ private void removePropertyStrings(Service s) { * * @since 1.5 */ - protected void removeService(Service s) { + protected synchronized void removeService(Service s) { checkInitialized(); if (debug != null) { debug.println(name + ".removeService(): " + s); @@ -1332,28 +2023,7 @@ protected void removeService(Service s) { if (s == null) { throw new NullPointerException(); } - implRemoveService(s); - } - - private void implRemoveService(Service s) { - if ((s == null) || serviceMap.isEmpty()) { - return; - } - String type = s.getType(); - String algorithm = s.getAlgorithm(); - ServiceKey key = new ServiceKey(type, algorithm, false); - Service oldService = serviceMap.get(key); - if (s != oldService) { - return; - } - servicesChanged = true; - serviceMap.remove(key); - for (String alias : s.getAliases()) { - serviceMap.remove(new ServiceKey(type, alias, false)); - } - - removePropertyStrings(s); - checkAndUpdateSecureRandom(type, algorithm, false); + servicesMap.asCurrent().removeService(s); } // Wrapped String that behaves in a case-insensitive way for equals/hashCode @@ -1502,9 +2172,21 @@ public static class Service { private String className; private final Provider provider; private List aliases; - private Map attributes; + private Map attributes; private final EngineDescription engineDescription; + // For services added to a ServicesMap, their algorithm service key. + // This value derives from the algorithm field. For services (still) + // not added to a ServicesMap, value is null. + private ServiceKey algKey; + + // For services added to a ServicesMap, this is a map from alias service + // keys to alias string values. Empty map if no aliases. While map + // entries derive from the aliases field, keys are not repeated + // (case-insensitive comparison) and not equal to the algorithm. For + // services (still) not added to a ServicesMap, value is an empty map. + private Map aliasKeys; + // Reference to the cached implementation Class object. // Will be a Class if this service is loaded from the built-in // classloader (unloading not possible), otherwise a WeakReference to a @@ -1533,52 +2215,99 @@ public static class Service { private static final Class[] CLASS0 = new Class[0]; - // this constructor and these methods are used for parsing - // the legacy string properties. - - private Service(Provider provider, String type, String algorithm) { + /* + * Constructor used from the ServicesMap Legacy API. + */ + private Service(Provider provider, ServiceKey algKey) { + assert algKey.algorithm.intern() == algKey.algorithm : + "Algorithm should be interned."; this.provider = provider; - this.type = type; - this.algorithm = algorithm; + this.algKey = algKey; + algorithm = algKey.originalAlgorithm; + type = algKey.type; engineDescription = knownEngines.get(type); aliases = Collections.emptyList(); + aliasKeys = Collections.emptyMap(); attributes = Collections.emptyMap(); } - private boolean isValid() { - return (type != null) && (algorithm != null) && (className != null); + /* + * Copy constructor used from the ServicesMap Legacy API for the + * copy-on-write strategy. This constructor is invoked after every + * update to a service on the ServicesMap. + */ + private Service(Service svc) { + provider = svc.provider; + type = svc.type; + algorithm = svc.algorithm; + algKey = svc.algKey; + className = svc.className; + engineDescription = svc.engineDescription; + if ((Object)svc.aliases == Collections.emptyList()) { + aliases = Collections.emptyList(); + aliasKeys = Collections.emptyMap(); + } else { + aliases = new ArrayList<>(svc.aliases); + aliasKeys = new HashMap<>(svc.aliasKeys); + } + if ((Object)svc.attributes == Collections.emptyMap()) { + attributes = Collections.emptyMap(); + } else { + attributes = new HashMap<>(svc.attributes); + } + registered = false; + + // Do not copy cached fields because the updated service may have a + // different class name or attributes and these values have to be + // regenerated. + classCache = null; + constructorCache = null; + hasKeyAttributes = null; + supportedFormats = null; + supportedClasses = null; } - private void addAlias(String alias) { - if (aliases.isEmpty()) { + /* + * Methods used from the ServicesMap Legacy API to update a service. + */ + + private void addAliasKey(ServiceKey aliasKey) { + assert !aliasKey.equals(algKey) : "Alias key cannot be equal to " + + "the algorithm."; + assert aliasKey.type.equals(type) : "Invalid alias key type."; + assert aliasKey.algorithm.intern() == aliasKey.algorithm : + "Alias should be interned."; + if ((Object)aliases == Collections.emptyList()) { aliases = new ArrayList<>(2); + aliasKeys = new HashMap<>(2); + } else if (aliasKeys.containsKey(aliasKey)) { + // When overwriting aliases, remove first to handle differences + // in alias string casing. + removeAliasKey(aliasKey); } - aliases.add(alias); + aliases.add(aliasKey.originalAlgorithm); + aliasKeys.put(aliasKey, aliasKey.originalAlgorithm); } - private void removeAlias(String alias) { - if (aliases.isEmpty()) { - return; - } - aliases.remove(alias); + private void removeAliasKey(ServiceKey aliasKey) { + assert aliasKeys.containsKey(aliasKey) && + aliases.contains(aliasKeys.get(aliasKey)) : + "Removing nonexistent alias."; + aliases.remove(aliasKeys.remove(aliasKey)); } - void addAttribute(String type, String value) { - if (attributes.isEmpty()) { + private void addAttribute(String attrName, String attrValue) { + if ((Object)attributes == Collections.emptyMap()) { attributes = new HashMap<>(8); } - attributes.put(new UString(type), value); + attributes.put(new UString(attrName), attrValue); } - void removeAttribute(String type, String value) { - if (attributes.isEmpty()) { - return; - } - if (value == null) { - attributes.remove(new UString(type)); - } else { - attributes.remove(new UString(type), value); - } + private void removeAttribute(String attrName, String attrValue) { + UString attrKey = new UString(attrName); + assert attributes.get(attrKey) == attrValue : + "Attribute value expected to exist with the same identity."; + attributes.remove(attrKey, attrValue); } /** @@ -1598,7 +2327,7 @@ void removeAttribute(String type, String value) { */ public Service(Provider provider, String type, String algorithm, String className, List aliases, - Map attributes) { + Map attributes) { if ((provider == null) || (type == null) || (algorithm == null) || (className == null)) { throw new NullPointerException(); @@ -1607,22 +2336,48 @@ public Service(Provider provider, String type, String algorithm, this.type = getEngineName(type); engineDescription = knownEngines.get(type); this.algorithm = algorithm; + algKey = null; this.className = className; if (aliases == null) { this.aliases = Collections.emptyList(); } else { this.aliases = new ArrayList<>(aliases); } + aliasKeys = Collections.emptyMap(); if (attributes == null) { this.attributes = Collections.emptyMap(); } else { this.attributes = new HashMap<>(); - for (Map.Entry entry : attributes.entrySet()) { - this.attributes.put(new UString(entry.getKey()), entry.getValue()); + for (Map.Entry entry : attributes.entrySet()) { + this.attributes.put(new UString(entry.getKey()), + entry.getValue()); } } } + /* + * When a Service is added to a ServicesMap with the Current API, + * service and alias keys must be generated. Currently used by + * ServicesMapImpl::putService. Legacy API methods do not need to call: + * they generated the algorithm key at construction time and alias + * keys with Service::addAliasKey. + */ + private void generateServiceKeys() { + if (algKey == null) { + assert (Object)aliasKeys == Collections.emptyMap() : + "aliasKeys expected to be the empty map."; + algKey = new ServiceKey(type, algorithm, true); + aliasKeys = new HashMap<>(aliases.size()); + for (String alias : aliases) { + ServiceKey aliasKey = new ServiceKey(type, alias, true); + if (!aliasKey.equals(algKey)) { + aliasKeys.put(aliasKey, alias); + } + } + aliasKeys = Collections.unmodifiableMap(aliasKeys); + } + } + /** * Get the type of this service. For example, {@code MessageDigest}. * @@ -1660,11 +2415,6 @@ public final String getClassName() { return className; } - // internal only - private List getAliases() { - return aliases; - } - /** * Return the value of the specified attribute or {@code null} if this * attribute is not set for this Service. diff --git a/test/jdk/java/security/Provider/ServicesConsistency.java b/test/jdk/java/security/Provider/ServicesConsistency.java new file mode 100644 index 0000000000000..50ecdb1a91993 --- /dev/null +++ b/test/jdk/java/security/Provider/ServicesConsistency.java @@ -0,0 +1,1109 @@ +/* + * Copyright (c) 2024, Red Hat, Inc. + * + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +import java.io.*; +import java.lang.reflect.Method; +import java.security.Key; +import java.security.Provider; +import java.security.Security; +import java.util.*; +import java.util.concurrent.atomic.AtomicReference; + +/* + * @test + * @bug 8345139 + * @run main/othervm/timeout=60 -enablesystemassertions ServicesConsistency + */ + +public final class ServicesConsistency { + + private static final String separatorThin = "----------------------------"; + + private static final String separatorThick = "============================"; + + private static final String aliasPrefix = "Alg.Alias."; + private static final String sT = "M"; + private static final String algL = "alg"; + private static final String alg2L = algL + "2"; + private static final String alg2U = alg2L.toUpperCase(); + private static final String algPropKeyL = sT + "." + algL; + private static final String alg2PropKeyL = sT + "." + alg2L; + private static final String algU = algL.toUpperCase(); + private static final String algPropKeyU = sT + "." + algU; + private static final String aliasL = "alias"; + private static final String aliasPropKeyL = aliasPrefix + sT + "." + aliasL; + private static final String aliasU = aliasL.toUpperCase(); + private static final String aliasPropKeyU = aliasPrefix + sT + "." + aliasU; + private static final String attrL = "attr1"; + private static final String attrU = attrL.toUpperCase(); + private static final String attrLAlgPropKeyL = algPropKeyL + " " + attrL; + private static final String attrLAlgPropKeyU = algPropKeyU + " " + attrL; + private static final String attrUAlgPropKeyL = algPropKeyL + " " + attrU; + private static final String attrUAlgPropKeyU = algPropKeyU + " " + attrU; + private static final String class1 = "class1"; + private static final String class2 = "class2"; + private static final String currentClass = "currentClass"; + private static final String currentClass2 = currentClass + "2"; + private static final String legacyClass = "legacyClass"; + private static final String attrValue = "attrValue"; + private static final String attrValue2 = attrValue + "2"; + private static Provider.Service s, s2; + + private static int testsFailed = 0; + private static int testsTotal = 0; + + private static final TestProvider p; + + static { + TestProvider tmp = new TestProvider(); + for (Provider p : Security.getProviders()) { + Security.removeProvider(p.getName()); + } + Security.addProvider(tmp); + p = tmp; + } + + private static final class TestProvider extends Provider { + @Serial + private static final long serialVersionUID = -6399263285569001970L; + + static TestProvider serializationCopy() throws Throwable { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + ObjectOutputStream oos = new ObjectOutputStream(baos); + oos.writeObject(p); + ObjectInputStream ois = new ObjectInputStream( + new ByteArrayInputStream(baos.toByteArray())); + return (TestProvider) ois.readObject(); + } + + TestProvider() { + super("TestProvider", "1.0", "TestProvider info"); + } + + void putService(String type, String algorithm, String className, + List aliases, Map attributes) { + System.out.println("Provider.putService(new Service(TestProvider," + + " " + type + ", " + algorithm + ", " + className + ", " + + aliases + ", " + attributes + "))"); + super.putService(new Service(this, type, algorithm, className, + aliases, attributes)); + } + + @Override + public void removeService(Provider.Service s) { + System.out.println("Provider.removeService(" + s + ")"); + super.removeService(s); + } + + @Override + public Object put(Object k, Object v) { + return put(k, v, true); + } + + Object put(Object k, Object v, boolean print) { + if (print) { + System.out.println("Provider.put(" + k + ", " + v + ")"); + } + return super.put(k, v); + } + + void showProperties() { + System.out.println(); + System.out.println("Properties map:"); + System.out.println(separatorThin); + for (Map.Entry e : entrySet()) { + Object k = e.getKey(); + Object v = e.getValue(); + if (k instanceof String ks) { + if (ks.startsWith("Provider.")) { + continue; + } + } + System.out.println(k + ": " + v); + } + System.out.println(); + } + } + + public static class TestSpi { + } + + public static class TestSpi2 { + } + + public static void main(String[] args) throws Throwable { + for (Method m : ServicesConsistency.class.getDeclaredMethods()) { + if (m.getName().startsWith("test")) { + try { + printTestHeader(m.getName()); + testsTotal += 1; + m.invoke(null); + } catch (Throwable t) { + testsFailed += 1; + t.printStackTrace(); + } finally { + p.clear(); + } + } + } + + if (testsFailed > 0) { + throw new Exception("TESTS FAILED: " + testsFailed + "/" + + testsTotal); + } else { + System.out.println("TESTS PASSED: " + testsTotal + "/" + + testsTotal); + } + } + + private static void printTestHeader(String testName) { + System.out.println(separatorThin); + System.out.println(testName); + System.out.println(separatorThin); + System.out.println(); + } + + private static void printThickHeader(String title) { + System.out.println(title); + System.out.println(separatorThick); + System.out.println(); + } + + private static void assertThat(boolean condition, String errorMsg) + throws Exception { + if (!condition) { + throw new Exception(errorMsg); + } + } + + private static void propValueAssertion(String propKey, String expectedValue, + String valueDesc, TestProvider p) throws Exception { + Object value = p.get(propKey); + assertThat(expectedValue.equals(value), "A wrong " + valueDesc + + " is assigned to the '" + propKey + "' provider property: " + + "expecting '" + expectedValue + "' but was '" + value + "'."); + } + + private static void assertClassnamePropValue(String propKey, + String expectedValue) throws Exception { + assertClassnamePropValue(propKey, expectedValue, p); + } + + private static void assertClassnamePropValue(String propKey, + String expectedValue, TestProvider p) throws Exception { + propValueAssertion(propKey, expectedValue, "class name", p); + } + + private static void assertAliasPropValue(String propKey, + String expectedValue) throws Exception { + propValueAssertion(propKey, expectedValue, "algorithm", p); + } + + private static void assertAttributePropValue(String propKey, + String expectedValue) throws Exception { + propValueAssertion(propKey, expectedValue, "attribute value", p); + } + + private static void assertPropRemoved(String propKey) throws Exception { + assertThat(p.get(propKey) == null, "Property '" + propKey + "' " + + "expected to be removed but was not. Current value is '" + + p.get(propKey) + "'."); + } + + private static String getServiceDesc(Provider.Service svc) { + return svc == null ? "null service" : "service with type '" + + svc.getType() + "' and algorithm '" + svc.getAlgorithm() + "'"; + } + + private static void serviceAssertionCommon(Provider.Service svc, + boolean isEqual, Provider.Service svc2, String errorMsg) + throws Exception { + String svc2Desc = getServiceDesc(svc2); + if (isEqual) { + assertThat(svc == svc2, errorMsg + " is not equal to a " + + svc2Desc + ", and was expected to be equal."); + } else { + assertThat(svc != svc2, errorMsg + " is equal to a " + svc2Desc + + ", and was not expected to be equal."); + } + } + + private static void lookupServiceAssertion(String type, String algorithm, + boolean isEqual, Provider.Service svc2) throws Exception { + serviceAssertionCommon(p.getService(type, algorithm), isEqual, svc2, + "A service looked up by type '" + type + "' and algorithm '" + + algorithm + "'"); + } + + private static void assertServiceEqual(String type, String algorithm, + Provider.Service svc2) throws Exception { + lookupServiceAssertion(type, algorithm, true, svc2); + } + + private static void assertServiceNotEqual(String type, String algorithm, + Provider.Service svc2) throws Exception { + lookupServiceAssertion(type, algorithm, false, svc2); + } + + private static void serviceAssertion(Provider.Service svc, boolean isEqual, + Provider.Service svc2) throws Exception { + serviceAssertionCommon(svc, isEqual, svc2, "A " + getServiceDesc(svc)); + } + + private static void assertServiceEqual(Provider.Service svc, + Provider.Service svc2) throws Exception { + serviceAssertion(svc, true, svc2); + } + + private static void assertServiceNotEqual(Provider.Service svc, + Provider.Service svc2) throws Exception { + serviceAssertion(svc, false, svc2); + } + + private static void assertClassname(Provider.Service svc, + String expectedClassName) throws Exception { + assertServiceNotEqual(svc, null); + String svcClassName = svc.getClassName(); + assertThat(expectedClassName.equals(svcClassName), "A " + + getServiceDesc(svc) + " was expected to have a class name " + + "equal to '" + expectedClassName + "' but is equal to '" + + svcClassName + "'."); + } + + private static void assertAttribute(Provider.Service svc, String attrName, + String expectedAttrValue) throws Exception { + assertServiceNotEqual(svc, null); + String attrValue = svc.getAttribute(attrName); + assertThat(Objects.equals(expectedAttrValue, attrValue), "A " + + getServiceDesc(svc) + " was expected to have a '" + attrName + + "' attribute equal to '" + expectedAttrValue + "' but" + + " is equal to '" + attrValue + "'."); + } + + private static void assertNotAttribute(Provider.Service svc, + String attrName) throws Exception { + assertAttribute(svc, attrName, null); + } + + private static void testBasicLegacyAPIOps() throws Throwable { + String attrLAliasPropKeyL = sT + "." + aliasL + " " + attrU; + + printThickHeader("Put an algorithm with two different cases:"); + p.put(algPropKeyL, class1); + p.put(algPropKeyU, class2); + p.showProperties(); + assertPropRemoved(algPropKeyL); + assertClassnamePropValue(algPropKeyU, class2); + assertClassname(p.getService(sT, algL), class2); + assertClassname(p.getService(sT, algU), class2); + p.clear(); + + printThickHeader("Assign an alias with two different cases:"); + p.put(algPropKeyL, class1); + p.put(aliasPropKeyL, algL); + p.put(aliasPropKeyU, algL); + s = p.getService(sT, algL); + p.showProperties(); + assertPropRemoved(aliasPropKeyL); + assertAliasPropValue(aliasPropKeyU, algL); + assertServiceEqual(sT, aliasL, s); + assertServiceEqual(sT, aliasU, s); + assertClassname(s, class1); + p.clear(); + + printThickHeader("Put an attribute with different algorithm cases:"); + p.put(algPropKeyL, class1); + p.put(attrLAlgPropKeyL, attrValue); + p.put(attrLAlgPropKeyU, attrValue2); + p.showProperties(); + assertPropRemoved(attrLAlgPropKeyL); + assertAttributePropValue(attrLAlgPropKeyU, attrValue2); + assertAttribute(p.getService(sT, algL), attrL, attrValue2); + assertAttribute(p.getService(sT, algU), attrU, attrValue2); + p.clear(); + + printThickHeader("Put an attribute with different attr name case:"); + p.put(algPropKeyL, class1); + p.put(attrLAlgPropKeyL, attrValue); + p.put(attrUAlgPropKeyL, attrValue2); + p.showProperties(); + assertPropRemoved(attrLAlgPropKeyL); + assertAttributePropValue(attrUAlgPropKeyL, attrValue2); + assertAttribute(p.getService(sT, algU), attrL, attrValue2); + assertAttribute(p.getService(sT, algL), attrU, attrValue2); + p.clear(); + + printThickHeader("Replace attribute by alias:"); + p.put(algPropKeyL, class1); + p.put(aliasPropKeyL, algL); + p.put(attrLAlgPropKeyL, attrValue); + p.put(attrLAliasPropKeyL, attrValue2); + p.showProperties(); + assertPropRemoved(attrLAliasPropKeyL); + assertAttributePropValue(attrLAlgPropKeyL, attrValue2); + assertAttribute(p.getService(sT, algL), attrL, attrValue2); + p.clear(); + + printThickHeader("Remove service:"); + p.put(algPropKeyL, class1); + p.put(aliasPropKeyL, algL); + p.put(attrLAlgPropKeyL, attrValue); + p.showProperties(); + p.remove(algPropKeyU); + assertClassnamePropValue(algPropKeyL, class1); + assertAliasPropValue(aliasPropKeyL, algL); + assertAttributePropValue(attrLAlgPropKeyL, attrValue); + assertClassname(p.getService(sT, algL), class1); + assertClassname(p.getService(sT, aliasL), class1); + assertAttribute(p.getService(sT, algL), attrL, attrValue); + p.remove(algPropKeyL); + assertPropRemoved(algPropKeyL); + assertPropRemoved(aliasPropKeyL); + assertPropRemoved(attrLAlgPropKeyL); + assertServiceEqual(sT, algL, null); + assertServiceEqual(sT, aliasL, null); + p.clear(); + + printThickHeader("Remove service alias:"); + p.put(algPropKeyL, class1); + p.put(aliasPropKeyL, algL); + p.remove(aliasPropKeyU); + assertClassnamePropValue(algPropKeyL, class1); + assertAliasPropValue(aliasPropKeyL, algL); + assertServiceNotEqual(sT, aliasL, null); + p.remove(aliasPropKeyL); + assertPropRemoved(aliasPropKeyL); + assertClassnamePropValue(algPropKeyL, class1); + assertServiceEqual(sT, aliasL, null); + assertClassname(p.getService(sT, algL), class1); + p.clear(); + + printThickHeader("Remove service attribute:"); + p.put(algPropKeyL, class1); + p.put(attrLAlgPropKeyL, attrValue); + p.remove(attrUAlgPropKeyL); + assertAttributePropValue(attrLAlgPropKeyL, attrValue); + assertAttribute(p.getService(sT, algL), attrL, attrValue); + p.remove(attrLAlgPropKeyU); + assertAttributePropValue(attrLAlgPropKeyL, attrValue); + assertAttribute(p.getService(sT, algL), attrL, attrValue); + p.remove(attrLAlgPropKeyL); + assertPropRemoved(attrLAlgPropKeyL); + assertClassnamePropValue(algPropKeyL, class1); + assertNotAttribute(p.getService(sT, algL), attrL); + } + + private static void testSerializationConsistencyBetweenAPIs() + throws Throwable { + printThickHeader("Before serialization:"); + p.putService(sT, algL, currentClass, null, null); + p.put(algPropKeyL, legacyClass); + p.showProperties(); + assertClassnamePropValue(algPropKeyL, currentClass); + assertClassname(p.getService(sT, algL), currentClass); + + TestProvider serialP = TestProvider.serializationCopy(); + + printThickHeader("After serialization:"); + serialP.showProperties(); + assertClassnamePropValue(algPropKeyL, currentClass, serialP); + assertClassname(serialP.getService(sT, algL), currentClass); + } + + private static void testComputeDoesNotThrowNPE() throws Throwable { + p.put(algPropKeyL, class1); + p.put(aliasPropKeyL, algL); + p.compute(aliasPropKeyL, (key, oldV) -> null); + assertPropRemoved(aliasPropKeyL); + } + + private static void testMergeDoesNotThrowNPE() throws Throwable { + p.put(algPropKeyL, class1); + p.put(aliasPropKeyL, algL); + p.merge(aliasPropKeyL, algL, (oldV, newV) -> null); + assertPropRemoved(aliasPropKeyL); + } + + private static void testLegacyAPIServicesOverride() throws Throwable { + legacyAPIServicesOverride(false); + } + + private static void testLegacyAPIServicesOverrideDifferentCase() + throws Throwable { + legacyAPIServicesOverride(true); + } + + private static void legacyAPIServicesOverride(boolean differentCase) + throws Throwable { + String aliasAsAlgPropKey = sT + "." + (differentCase ? aliasU : aliasL); + String algAsAliasPropKey = aliasPrefix + sT + "." + + (differentCase ? algU : algL); + + printThickHeader("A Legacy API service algorithm can override a " + + "Legacy API service algorithm:"); + p.put(algPropKeyL, class1); + p.put(aliasPropKeyL, algL); + p.put(attrLAlgPropKeyL, attrValue); + assertClassnamePropValue(algPropKeyL, class1); + assertAliasPropValue(aliasPropKeyL, algL); + assertAttributePropValue(attrLAlgPropKeyL, attrValue); + s = p.getService(sT, algL); + assertServiceEqual(sT, aliasL, s); + assertClassname(s, class1); + assertAttribute(s, attrL, attrValue); + p.put(differentCase ? algPropKeyU : algPropKeyL, class2); + p.showProperties(); + s2 = p.getService(sT, algL); + assertClassnamePropValue(differentCase ? algPropKeyU : algPropKeyL, + class2); + if (differentCase) { + assertPropRemoved(algPropKeyL); + } + assertAliasPropValue(aliasPropKeyL, algL); + assertAliasPropValue(attrLAlgPropKeyL, attrValue); + assertServiceEqual(sT, algU, s2); + assertClassname(s2, class2); + assertClassname(p.getService(sT, aliasL), class2); + assertAttribute(s2, attrL, attrValue); + p.clear(); + + printThickHeader("A Legacy API service algorithm is a Legacy API " + + "service alias already. Modify the existing service through " + + "its alias:"); + p.put(algPropKeyL, class1); + p.put(attrLAlgPropKeyL, attrValue); + p.put(aliasPropKeyL, algL); + p.put(aliasAsAlgPropKey, class2); + p.showProperties(); + s = p.getService(sT, algL); + assertClassnamePropValue(algPropKeyL, class2); + assertPropRemoved(aliasAsAlgPropKey); + assertAttributePropValue(attrLAlgPropKeyL, attrValue); + assertAliasPropValue(aliasPropKeyL, algL); + assertClassname(p.getService(sT, aliasL), class2); + assertClassname(p.getService(sT, aliasU), class2); + assertClassname(s, class2); + assertAttribute(s, attrL, attrValue); + p.clear(); + + printThickHeader("A Legacy API service alias can override a Legacy " + + "API service alias:"); + p.put(algPropKeyL, class1); + p.put(aliasPropKeyL, algL); + p.put(alg2PropKeyL, class2); + p.put(differentCase ? aliasPropKeyU : aliasPropKeyL, alg2L); + p.showProperties(); + s2 = p.getService(sT, alg2L); + assertAliasPropValue(differentCase ? aliasPropKeyU : aliasPropKeyL, + alg2L); + if (differentCase) { + assertPropRemoved(aliasPropKeyL); + } + assertServiceEqual(sT, aliasL, s2); + assertServiceEqual(sT, aliasU, s2); + assertClassname(p.getService(sT, algL), class1); + assertClassname(s2, class2); + p.clear(); + + printThickHeader("A Legacy API service algorithm cannot be " + + "overwritten by a Legacy API service alias:"); + p.put(algPropKeyL, class1); + s = p.getService(sT, algL); + p.put(alg2PropKeyL, class2); + p.put(algAsAliasPropKey, alg2L); + s2 = p.getService(sT, alg2L); + p.showProperties(); + assertPropRemoved(algAsAliasPropKey); + assertClassnamePropValue(algPropKeyL, class1); + assertClassnamePropValue(alg2PropKeyL, class2); + assertServiceEqual(sT, algL, s); + assertServiceEqual(sT, algU, s); + assertClassname(s, class1); + assertClassname(s2, class2); + p.clear(); + + // Add a Current API service to test invalid overrides + p.putService(sT, algL, currentClass, List.of(aliasL), + Map.of(attrL, attrValue)); + s = p.getService(sT, algL); + assertServiceNotEqual(s, null); + assertServiceEqual(sT, aliasL, s); + System.out.println(); + + printThickHeader("A Legacy API service algorithm cannot overwrite a " + + "Current API service algorithm:"); + p.put(differentCase ? algPropKeyU : algPropKeyL, legacyClass); + p.showProperties(); + assertClassnamePropValue(algPropKeyL, currentClass); + if (differentCase) { + assertPropRemoved(algPropKeyU); + } + assertClassname(p.getService(sT, algL), currentClass); + assertClassname(p.getService(sT, algU), currentClass); + + printThickHeader("A Legacy API service alias cannot overwrite a " + + "Current API service alias:"); + p.put(differentCase ? aliasPropKeyU : aliasPropKeyL, alg2L); + p.showProperties(); + assertAliasPropValue(aliasPropKeyL, algL); + if (differentCase) { + assertPropRemoved(aliasPropKeyU); + } + assertServiceEqual(sT, aliasL, s); + assertClassname(p.getService(sT, aliasL), currentClass); + assertClassname(p.getService(sT, aliasU), currentClass); + + printThickHeader("A Legacy API service cannot overwrite a Current API" + + " service attribute:"); + p.put(differentCase ? attrUAlgPropKeyU : attrLAlgPropKeyL, attrValue2); + p.showProperties(); + assertAttributePropValue(attrLAlgPropKeyL, attrValue); + if (differentCase) { + assertPropRemoved(attrUAlgPropKeyU); + } + assertAttribute(p.getService(sT, algL), attrL, attrValue); + + printThickHeader("A Legacy API service alias cannot overwrite a " + + "Current API service algorithm:"); + p.put(algAsAliasPropKey, alg2L); + p.showProperties(); + assertClassnamePropValue(algPropKeyL, currentClass); + assertPropRemoved(algAsAliasPropKey); + assertClassname(p.getService(sT, algL), currentClass); + assertClassname(p.getService(sT, algU), currentClass); + + printThickHeader("A Legacy API service algorithm cannot overwrite a " + + "Current API service alias:"); + p.put(aliasAsAlgPropKey, legacyClass); + p.showProperties(); + assertAliasPropValue(aliasPropKeyL, algL); + assertPropRemoved(aliasAsAlgPropKey); + assertClassname(p.getService(sT, aliasL), currentClass); + assertClassname(p.getService(sT, aliasU), currentClass); + + assertServiceEqual(p.getService(sT, algL), s); + assertServiceEqual(p.getService(sT, aliasL), s); + } + + private static void testLegacyAPIAliasCannotBeAlgorithm() throws Throwable { + p.put(aliasPropKeyL, aliasL); + p.showProperties(); + assertPropRemoved(aliasPropKeyL); + p.clear(); + + p.put(sT + "." + aliasU, class1); + p.put(aliasPropKeyL, aliasU); + p.showProperties(); + assertClassnamePropValue(sT + "." + aliasU, class1); + assertPropRemoved(aliasPropKeyL); + } + + private static void testCurrentAPIServicesOverride() throws Throwable { + currentAPIServicesOverride(false); + } + + private static void testCurrentAPIServicesOverrideDifferentCase() + throws Throwable { + currentAPIServicesOverride(true); + } + + private static void currentAPIServicesOverride(boolean differentCase) + throws Throwable { + printThickHeader("A Current API service overrides a Legacy API " + + "service algorithm with its algorithm:"); + p.put(algPropKeyL, legacyClass); + p.put(attrLAlgPropKeyL, attrValue); + p.put(aliasPropKeyL, algL); + s = p.getService(sT, algL); + p.putService(sT, differentCase ? algU : algL, currentClass, List.of(), + null); + s2 = p.getService(sT, differentCase ? algU : algL); + p.showProperties(); + if (differentCase) { + assertPropRemoved(algPropKeyL); + } + assertPropRemoved(attrLAlgPropKeyL); + assertPropRemoved(aliasPropKeyL); + assertClassnamePropValue(differentCase ? algPropKeyU : algPropKeyL, + currentClass); + assertClassname(s, legacyClass); + assertClassname(s2, currentClass); + assertServiceEqual(sT, aliasL, null); + assertNotAttribute(s2, attrL); + p.clear(); + + printThickHeader("A Current API service overrides a Legacy API " + + "service alias with its algorithm:"); + p.put(algPropKeyL, legacyClass); + p.put(aliasPropKeyL, algL); + s = p.getService(sT, algL); + assertServiceEqual(sT, aliasL, s); + p.putService(sT, differentCase ? aliasU : aliasL, currentClass, + List.of(), null); + s2 = p.getService(sT, differentCase ? aliasU : aliasL); + p.showProperties(); + assertClassnamePropValue(algPropKeyL, legacyClass); + assertClassnamePropValue(sT + "." + (differentCase ? aliasU : aliasL), + currentClass); + assertPropRemoved(aliasPropKeyL); + assertClassname(p.getService(sT, algL), legacyClass); + assertClassname(s2, currentClass); + p.clear(); + + printThickHeader("A Current API service overrides a Legacy API " + + "service algorithm with its alias:"); + p.put(algPropKeyL, legacyClass); + p.put(aliasPropKeyL, algL); + p.put(attrLAlgPropKeyL, attrValue); + s = p.getService(sT, algL); + assertServiceEqual(sT, aliasL, s); + p.putService(sT, alg2L, currentClass, + List.of(differentCase ? algU : algL), null); + s2 = p.getService(sT, alg2L); + p.showProperties(); + assertClassnamePropValue(alg2PropKeyL, currentClass); + assertAliasPropValue(aliasPrefix + sT + "." + (differentCase ? algU : + algL), alg2L); + assertPropRemoved(algPropKeyL); + assertPropRemoved(aliasPropKeyL); + assertPropRemoved(attrLAlgPropKeyL); + assertServiceEqual(sT, aliasL, null); + assertServiceNotEqual(s, s2); + assertServiceEqual(sT, algL, s2); + assertClassname(s2, currentClass); + assertNotAttribute(s2, attrL); + p.clear(); + + printThickHeader("A Current API service overrides a Legacy API alias" + + " with its alias:"); + p.put(algPropKeyL, legacyClass); + p.put(aliasPropKeyL, algL); + p.put(attrLAlgPropKeyL, attrValue); + s = p.getService(sT, algL); + assertServiceEqual(sT, aliasL, s); + p.putService(sT, alg2L, currentClass, List.of(differentCase ? + aliasU : aliasL), null); + s2 = p.getService(sT, alg2L); + s = p.getService(sT, algL); + p.showProperties(); + assertClassnamePropValue(algPropKeyL, legacyClass); + assertAttributePropValue(attrLAlgPropKeyL, attrValue); + assertClassnamePropValue(alg2PropKeyL, currentClass); + assertAliasPropValue(differentCase ? aliasPropKeyU : aliasPropKeyL, + alg2L); + if (differentCase) { + assertPropRemoved(aliasPropKeyL); + } + assertClassname(s, legacyClass); + assertServiceEqual(sT, aliasL, s2); + assertClassname(s2, currentClass); + assertNotAttribute(s2, attrL); + assertAttribute(s, attrL, attrValue); + p.clear(); + + printThickHeader("A Current API service overrides a Current API " + + "service algorithm with its algorithm:"); + p.putService(sT, algL, currentClass, List.of(aliasL), Map.of(attrL, + attrValue)); + s = p.getService(sT, algL); + assertServiceEqual(sT, aliasL, s); + assertClassname(s, currentClass); + assertAttribute(s, attrL, attrValue); + p.putService(sT, differentCase ? algU : algL, currentClass2, List.of(), + null); + s2 = p.getService(sT, differentCase ? algU : algL); + p.showProperties(); + assertClassnamePropValue(differentCase ? algPropKeyU : algPropKeyL, + currentClass2); + if (differentCase) { + assertPropRemoved(algPropKeyL); + } + assertPropRemoved(aliasPropKeyL); + assertPropRemoved(attrLAlgPropKeyL); + assertClassname(s2, currentClass2); + assertNotAttribute(s2, attrL); + assertServiceEqual(sT, aliasL, null); + p.clear(); + + printThickHeader("A Current API service overrides a Current API " + + "service alias with its algorithm:"); + p.putService(sT, algL, currentClass, List.of(alg2L), Map.of(attrL, + attrValue)); + assertServiceEqual(sT, alg2L, p.getService(sT, algL)); + p.putService(sT, differentCase ? alg2U : alg2L, currentClass2, + List.of(), null); + s = p.getService(sT, algL); + s2 = p.getService(sT, differentCase ? alg2U : alg2L); + p.showProperties(); + assertClassnamePropValue(algPropKeyL, currentClass); + assertAttributePropValue(attrLAlgPropKeyL, attrValue); + assertClassnamePropValue(sT + "." + (differentCase ? alg2U : alg2L), + currentClass2); + assertPropRemoved(aliasPrefix + alg2PropKeyL); + assertClassname(s, currentClass); + assertClassname(s2, currentClass2); + assertAttribute(s, attrL, attrValue); + assertNotAttribute(s2, attrL); + p.removeService(s); + assertPropRemoved(algPropKeyL); + assertPropRemoved(attrLAlgPropKeyL); + assertClassnamePropValue(sT + "." + (differentCase ? alg2U : alg2L), + currentClass2); + assertServiceEqual(sT, algL, null); + assertServiceEqual(s2, p.getService(sT, differentCase ? alg2U : alg2L)); + p.clear(); + + printThickHeader("A Current API service overrides a Current API " + + "service algorithm with its alias:"); + p.putService(sT, algL, currentClass, List.of(aliasL), Map.of(attrL, + attrValue)); + s = p.getService(sT, algL); + assertServiceEqual(sT, aliasL, s); + assertAttribute(s, attrL, attrValue); + p.putService(sT, alg2L, currentClass2, List.of(differentCase ? + algU : algL), null); + s2 = p.getService(sT, alg2L); + p.showProperties(); + assertClassnamePropValue(alg2PropKeyL, currentClass2); + assertAliasPropValue(aliasPrefix + sT + "." + (differentCase ? + algU : algL), alg2L); + assertPropRemoved(algPropKeyL); + assertPropRemoved(aliasPropKeyL); + assertPropRemoved(attrLAlgPropKeyL); + assertServiceEqual(sT, algL, s2); + assertServiceEqual(sT, algU, s2); + assertClassname(s2, currentClass2); + assertNotAttribute(s2, attrL); + assertServiceEqual(sT, aliasL, null); + p.clear(); + + printThickHeader("A Current API service overrides a Current API " + + "service alias with its alias:"); + p.putService(sT, algL, currentClass, List.of(aliasL), Map.of(attrL, + attrValue)); + s = p.getService(sT, algL); + assertServiceEqual(sT, aliasL, s); + assertAttribute(s, attrL, attrValue); + p.putService(sT, alg2L, currentClass2, List.of(differentCase ? + aliasU : aliasL), null); + s2 = p.getService(sT, alg2L); + p.showProperties(); + assertClassnamePropValue(algPropKeyL, currentClass); + assertClassnamePropValue(alg2PropKeyL, currentClass2); + assertAliasPropValue(differentCase ? aliasPropKeyU : aliasPropKeyL, + alg2L); + if (differentCase) { + assertPropRemoved(aliasPropKeyL); + } + assertAttributePropValue(attrLAlgPropKeyL, attrValue); + assertServiceEqual(sT, algL, s); + assertServiceEqual(sT, aliasL, s2); + assertServiceEqual(sT, aliasU, s2); + p.removeService(s); + assertPropRemoved(algPropKeyL); + assertPropRemoved(attrLAlgPropKeyL); + assertAliasPropValue(differentCase ? aliasPropKeyU : aliasPropKeyL, + alg2L); + assertServiceEqual(sT, algL, null); + assertServiceEqual(sT, aliasL, s2); + assertServiceEqual(sT, aliasU, s2); + } + + private static void testInvalidServiceNotReturned() throws Throwable { + p.put(aliasPropKeyL, algL); + assertServiceEqual(sT, algL, null); + assertServiceEqual(sT, aliasL, null); + } + + private static void testInvalidCachedHasKeyAttributes() throws Throwable { + invalidCachedSupportedKeyFormats(true); + } + + private static void testInvalidCachedSupportedKeyFormats() + throws Throwable { + invalidCachedSupportedKeyFormats(false); + } + + private static void invalidCachedSupportedKeyFormats( + boolean targetingHasAttributes) throws Throwable { + String sT = "Cipher"; + String algPropKeyL = sT + "." + algL; + String attrPropKey = algPropKeyL + " SupportedKeyFormats"; + String format1 = "format1"; + String format2 = "format2"; + boolean supportsKeyFormat, supportsKeyFormat2; + Key key = new Key() { + @Serial + private static final long serialVersionUID = 5040566397999588441L; + @Override + public String getAlgorithm() { + return null; + } + @Override + public String getFormat() { + return format2; + } + @Override + public byte[] getEncoded() { + return new byte[0]; + } + }; + + p.put(algPropKeyL, TestSpi.class.getName()); + if (!targetingHasAttributes) { + p.put(attrPropKey, format2); + } + supportsKeyFormat = p.getService(sT, algL).supportsParameter(key); + p.put(attrPropKey, format1); + supportsKeyFormat2 = p.getService(sT, algL).supportsParameter(key); + p.showProperties(); + + assertClassnamePropValue(algPropKeyL, TestSpi.class.getName()); + assertAttributePropValue(attrPropKey, format1); + assertThat(supportsKeyFormat && !supportsKeyFormat2, + "supportsKeyFormat expected to be 'true' (was '" + + supportsKeyFormat + "'), and supportsKeyFormat2 expected to " + + "be 'false' (was '" + supportsKeyFormat2 + "')."); + } + + private static void testInvalidCachedClass() throws Throwable { + Object testSpi, testSpi2; + p.put(algPropKeyL, TestSpi.class.getName()); + s = p.getService(sT, algL); + testSpi = s.newInstance(null); + assertClassname(s, TestSpi.class.getName()); + p.put(algPropKeyL, TestSpi2.class.getName()); + s2 = p.getService(sT, algL); + testSpi2 = s2.newInstance(null); + assertClassname(s2, TestSpi2.class.getName()); + p.showProperties(); + + assertClassnamePropValue(algPropKeyL, TestSpi2.class.getName()); + assertThat(testSpi.getClass() == TestSpi.class && testSpi2.getClass() + == TestSpi2.class, "testSpi expected to be an instance of '" + + TestSpi.class.getSimpleName() + "' (was an instance of '" + + testSpi.getClass().getSimpleName() + "'), and testSpi2 " + + "expected to be an instance of '" + + TestSpi2.class.getSimpleName() + "' (was an instance of '" + + testSpi2.getClass().getSimpleName() + "')."); + } + + private static void testLegacyAPIAddIsRemove() throws Throwable { + Object obj = new Object(); + p.put(algPropKeyL, class1); + p.put(algPropKeyU, obj); + assertClassnamePropValue(algPropKeyL, class1); + assertServiceNotEqual(sT, algL, null); + p.showProperties(); + p.put(algPropKeyL, obj); + p.showProperties(); + assertThat(p.get(algPropKeyL) == obj, "Entry value " + + "expected to be the Object instance added."); + assertThat(p.get(algPropKeyU) == obj, "Entry value " + + "expected to be the Object instance added."); + assertServiceEqual(sT, algL, null); + } + + private static void testReplaceAllIsAtomic() throws Throwable { + concurrentReadWrite((Map aliases) -> { + p.put(alg2PropKeyL, class2); + p.putAll(aliases); + }, + (Map aliases) -> + p.replaceAll((k, v) -> { + if (((String)k).startsWith(aliasPrefix) && + algL.equals(v)) { + return alg2L; + } + return v; + }), + (String sT, String firstAlias, String lastAlias) -> { + Provider.Service s1 = p.getService(sT, firstAlias); + Provider.Service s2 = p.getService(sT, lastAlias); + if (s1 != null && s1.getClassName().equals(class2)) { + if (s2 == null) { + throw new Exception("First service found, " + + "last service not found."); + } + return true; + } + return false; + }, "Provider::replaceAll is not atomic."); + } + + private static void testPutAllIsAtomic() throws Throwable { + concurrentReadWrite(null, p::putAll, + (String sT, String firstAlias, String lastAlias) -> { + Provider.Service s1 = p.getService(sT, firstAlias); + Provider.Service s2 = p.getService(sT, lastAlias); + if (s1 != null) { + if (s2 == null) { + throw new Exception("First service found, " + + "last service not found."); + } + return true; + } + return false; + }, "Provider::putAll is not atomic."); + } + + private static void testConcurrentServiceModification() throws Throwable { + concurrentReadWrite(null, (Map aliases) -> { + for (Map.Entry aliasEntry : aliases.entrySet()) { + p.put(aliasEntry.getKey(), aliasEntry.getValue(), false); + } + }, (String sT, String firstAlias, String lastAlias) -> { + Provider.Service s1 = p.getService(sT, firstAlias); + if (s1 != null) { + s1.toString(); + } + return p.getService(sT, lastAlias) != null; + }, "Concurrent modification of a service compromised integrity."); + } + + @FunctionalInterface + private interface ConcurrentWriteCallback { + void apply(Map aliases); + } + + @FunctionalInterface + private interface ConcurrentReadCallback { + boolean apply(String sT, String firstAlias, String lastAlias) + throws Throwable; + } + + private static void concurrentReadWrite(ConcurrentWriteCallback preWriteCB, + ConcurrentWriteCallback writerCB, ConcurrentReadCallback readerCB, + String errorMsg) throws Throwable { + int lastAlias = 500; + int numberOfExperiments = 10; + String firstAliasL = aliasL + 0; + String lastAliasL = aliasL + lastAlias; + Map aliasesMap = new LinkedHashMap<>(lastAlias + 1); + for (int i = 0; i <= lastAlias; i++) { + aliasesMap.put(aliasPropKeyL + i, algL); + } + AtomicReference exceptionRef = new AtomicReference<>(null); + Runnable writerR = () -> writerCB.apply(aliasesMap); + Runnable readerR = () -> { + try { + while (!readerCB.apply(sT, firstAliasL, lastAliasL)); + } catch (Throwable t) { + exceptionRef.set(t); + } + }; + for (int i = 0; i < numberOfExperiments; i++) { + p.clear(); + p.put(algPropKeyL, class1); + Thread writerT = new Thread(writerR); + Thread readerT = new Thread(readerR); + if (preWriteCB != null) { + preWriteCB.apply(aliasesMap); + } + readerT.start(); + writerT.start(); + writerT.join(); + readerT.join(); + if (exceptionRef.get() != null) { + throw new Exception(errorMsg, exceptionRef.get()); + } + } + } + + private static void testInvalidGetServiceRemoval() throws Throwable { + invalidServiceRemoval(true); + } + + private static void testInvalidGetServicesRemoval() throws Throwable { + invalidServiceRemoval(false); + } + + private static void invalidServiceRemoval(boolean getService) + throws Throwable { + p.put(aliasPropKeyL, algL); + if (getService) { + p.getService(sT, aliasL); + } else { + p.getServices(); + } + p.put(algPropKeyL, class1); + p.showProperties(); + + assertAliasPropValue(aliasPropKeyL, algL); + assertClassnamePropValue(algPropKeyL, class1); + s = p.getService(sT, aliasL); + assertClassname(s, class1); + assertServiceEqual(sT, algL, s); + } + + private static void testSerializationClassnameConsistency() + throws Throwable { + printThickHeader("Before serialization:"); + p.put(algPropKeyU, class1); + p.put(algPropKeyL, class2); + s = p.getService(sT, algL); + s2 = p.getService(sT, algU); + p.showProperties(); + assertClassname(s, class2); + assertClassname(s2, class2); + + TestProvider serialP = TestProvider.serializationCopy(); + + printThickHeader("After serialization:"); + serialP.showProperties(); + s = serialP.getService(sT, algL); + s2 = serialP.getService(sT, algU); + p.showProperties(); + assertClassname(s, class2); + assertClassname(s2, class2); + } + + private static void testCreateServiceByAlias() throws Throwable { + printThickHeader("Create service by alias and set classname by alias:"); + p.put(aliasPropKeyL, algL); + assertServiceEqual(sT, algL, null); + assertServiceEqual(sT, aliasL, null); + p.put(sT + "." + aliasL, class1); + p.showProperties(); + assertPropRemoved(sT + "." + aliasL); + assertAliasPropValue(aliasPropKeyL, algL); + assertClassnamePropValue(algPropKeyL, class1); + s = p.getService(sT, algL); + assertServiceEqual(sT, aliasL, s); + assertClassname(s, class1); + } + + private static void testCreateServiceByAttr() throws Throwable { + printThickHeader("Create a service by attribute:"); + p.put(attrLAlgPropKeyL, attrValue); + assertServiceEqual(sT, algL, null); + p.put(algPropKeyL, class1); + p.showProperties(); + assertAttributePropValue(attrLAlgPropKeyL, attrValue); + assertClassnamePropValue(algPropKeyL, class1); + assertAttribute(p.getService(sT, algL), attrL, attrValue); + } +} From bda8971f15cd91baa029313e2b141d6af78d9068 Mon Sep 17 00:00:00 2001 From: Martin Balao Date: Wed, 8 Jan 2025 13:28:50 -0300 Subject: [PATCH 2/7] Copyright update. Co-authored-by: Martin Balao Alonso Co-authored-by: Francisco Ferrari Bihurriet --- src/java.base/share/classes/java/security/Provider.java | 2 +- test/jdk/java/security/Provider/ServicesConsistency.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/java.base/share/classes/java/security/Provider.java b/src/java.base/share/classes/java/security/Provider.java index 50f0de809c4bf..910d716038e3b 100644 --- a/src/java.base/share/classes/java/security/Provider.java +++ b/src/java.base/share/classes/java/security/Provider.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 1996, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 1996, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it diff --git a/test/jdk/java/security/Provider/ServicesConsistency.java b/test/jdk/java/security/Provider/ServicesConsistency.java index 50ecdb1a91993..78873559b3fdc 100644 --- a/test/jdk/java/security/Provider/ServicesConsistency.java +++ b/test/jdk/java/security/Provider/ServicesConsistency.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2024, Red Hat, Inc. + * Copyright (c) 2025, Red Hat, Inc. * * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * From ac45d7c4afa58df2c50d8e24f906457f41c6bb81 Mon Sep 17 00:00:00 2001 From: Martin Balao Date: Tue, 11 Feb 2025 18:10:58 -0500 Subject: [PATCH 3/7] ServicesMapImpl class and Current, Legacy interfaces removed. Co-authored-by: Martin Balao Alonso Co-authored-by: Francisco Ferrari Bihurriet --- .../share/classes/java/security/Provider.java | 1343 ++++++++--------- 1 file changed, 617 insertions(+), 726 deletions(-) diff --git a/src/java.base/share/classes/java/security/Provider.java b/src/java.base/share/classes/java/security/Provider.java index 910d716038e3b..8f53312ca8fc7 100644 --- a/src/java.base/share/classes/java/security/Provider.java +++ b/src/java.base/share/classes/java/security/Provider.java @@ -705,540 +705,426 @@ private void checkInitialized() { */ private final class ServicesMap { /* - * Enum to inform the result of an operation on the services map. + * Record to aggregate information about the lookup of a service on the + * internal map. See ServicesMap::find for a description of possible + * values. */ - enum SvcOpResult { - SUCCESS, - ERROR + private record MappingInfo(Service svc, ServiceKey algKey, + Boolean isLegacy) {} + + // Placeholder for a thread to mark that serviceSet values are being + // computed after a services update. Only one thread at a time can + // effectively assign this value. + private static final Set SERVICE_SET_IN_PROGRESS = Set.of(); + + // Unmodifiable set of all services. Possible values for this field + // are: 1) null (indicates that the set has to be recomputed after a + // service update), 2) SERVICE_SET_IN_PROGRESS (indicates that a thread + // is recomputing its value), and 3) an actual set of services. + private final AtomicReference> serviceSet; + + // The internal services map, containing services registered with the + // Current and the Legacy APIs. Concurrent read and write access to this + // map is expected. Both algorithm and alias service keys are added to + // this map. + private final Map services; + + // Auxiliary set to determine if a service on the services map was added + // with the Legacy API. The absence of a service key on this set is an + // indication that the service was either not added or added with the + // Current API. Only algorithm service keys are added to this set. + private final Set legacySvcKeys; + + // Auxiliary map to keep track of the Properties map entries that + // originated entries on the internal map. This information is used to + // avoid inconsistencies. Both algorithm and alias service keys are + // added to this map. + private final Map serviceProps; + + // Auxiliary map to keep track of the Properties map entries that + // originated service attributes on the internal map. This information + // is used to avoid inconsistencies. Only algorithm service keys are + // added to this map. + private final Map> serviceAttrProps; + + ServicesMap() { + serviceSet = new AtomicReference<>(); + services = new ConcurrentHashMap<>(); + legacySvcKeys = new HashSet<>(); + serviceProps = new HashMap<>(); + serviceAttrProps = new HashMap<>(); } /* - * Interface to add and remove services to the map according to the - * Current API. These functions update the Properties map to reflect - * service changes, including algorithms, aliases and attributes. - * - * Services added with the Legacy API may be overwritten with this API. - * - * This interface guarantees atomicity from a service reader point - * of view. In other words, a reader that gets a service will see all - * its attributes and aliases as they were at time of registration. + * Constructor to create a thin working copy such that readers of the + * original map do not notice any new changes. Used for atomic changes + * with the Legacy API. See Providers::putAll. */ - interface Current { - SvcOpResult putService(Service svc); - SvcOpResult removeService(Service svc); + ServicesMap(ServicesMap original) { + serviceSet = new AtomicReference<>(original.serviceSet.get()); + services = new ConcurrentHashMap<>(original.services); + legacySvcKeys = original.legacySvcKeys; + serviceProps = original.serviceProps; + serviceAttrProps = original.serviceAttrProps; } /* - * Interface to add, modify and remove services on the map according to - * the Legacy API. These functions update the Properties map to reflect - * service changes, including algorithms, aliases and attributes. - * - * Services added with the Current API cannot be overwritten with - * this API. - * - * Notice that this interface does not guarantee atomicity in a - * sequence of operations from a service reader point of view. As - * an example, a service reader may get a service missing an attribute - * if looked up between a writer's putClassName() and putAttribute() - * calls. For atomic changes with the Legacy API see Provider::putAll. + * Returns an unmodifiable set of available services. Recomputes + * serviceSet if needed, after a service update. Both services added + * with the Current and Legacy APIs are included. If no services are + * found, the returned set is empty. This method is thread-safe and + * lock-free. */ - interface Legacy { - SvcOpResult putClassName(ServiceKey key, String className, - String propKey); - SvcOpResult putAlias(ServiceKey key, ServiceKey aliasKey, - String propKey); - SvcOpResult putAttribute(ServiceKey key, String attrName, - String attrValue, String propKey); - SvcOpResult remove(ServiceKey key, String className); - SvcOpResult removeAlias(ServiceKey key, ServiceKey aliasKey); - SvcOpResult removeAttribute(ServiceKey key, String attrName, - String attrValue); + Set getServices() { + Set serviceSetLocal = serviceSet.compareAndExchange( + null, SERVICE_SET_IN_PROGRESS); + if (serviceSetLocal == null || + serviceSetLocal == SERVICE_SET_IN_PROGRESS) { + // A cached set is not available. Instead of locking, compute + // the set to be returned and, eventually, make it available + // for others to use. + Set newSet = computeServicesSet(); + if (serviceSetLocal == null) { + // We won the race to make the computed set available for + // others to use. However, only make it available if it + // is still current (in other words, there were no further + // changes). If it is not current, the next reader will + // do the job. + serviceSet.compareAndExchange( + SERVICE_SET_IN_PROGRESS, newSet); + } + serviceSetLocal = newSet; + } + return serviceSetLocal; + } + + private Set computeServicesSet() { + Set set = new LinkedHashSet<>(); + for (Map.Entry e : services.entrySet()) { + Service svc = e.getValue(); + // Skip alias based entries and filter out invalid services. + // + // Note: Multiple versions of the same service (reflecting + // different points in time) can be generated by concurrent + // writes with the Legacy API and, as a result of the + // copy-on-write strategy, seen under different service keys + // here. Each version has a unique object identity and, thus, + // would be distinguishable for a Set set. To avoid + // duplicates, we skip alias keys and use the version of the + // service pointed by the algorithm key. + if (e.getKey().equals(svc.algKey) && isValid(svc)) { + set.add(svc); + } + } + return Collections.unmodifiableSet(set); } /* - * This class is the internal implementation of the services map. - * Services can be added or removed either through the Current or the - * Legacy API. + * Returns an available service. Both services added with the Current + * and Legacy APIs are considered in the search. Thread-safe and + * lock-free. */ - private final class ServicesMapImpl implements Current, Legacy { - /* - * Record to aggregate information about the lookup of a service on - * the internal map. See ServicesMapImpl::find for a description of - * possible values. - */ - private record MappingInfo(Service svc, ServiceKey algKey, - Boolean isLegacy) {} - - // The internal services map, containing services registered with - // the Current and the Legacy APIs. Concurrent read and write access - // to this map is expected. Both algorithm and alias service keys - // are added to this map. - private final Map services; - - // Auxiliary set to determine if a service on the services map - // was added with the Legacy API. The absence of a service key - // on this set is an indication that the service was either not - // added or added with the Current API. Only algorithm service keys - // are added to this set. - private final Set legacySvcKeys; - - // Auxiliary map to keep track of the Properties map entries that - // originated entries on the internal map. This information is used - // to avoid inconsistencies. Both algorithm and alias service keys - // are added to this map. - private final Map serviceProps; - - // Auxiliary map to keep track of the Properties map entries that - // originated service attributes on the internal map. This - // information is used to avoid inconsistencies. Only algorithm - // service keys are added to this map. - private final Map> - serviceAttrProps; - - ServicesMapImpl() { - services = new ConcurrentHashMap<>(); - legacySvcKeys = new HashSet<>(); - serviceProps = new HashMap<>(); - serviceAttrProps = new HashMap<>(); - } + Service getService(ServiceKey key) { + Service svc = services.get(key); + return svc != null && isValid(svc) ? svc : null; + } - /* - * Constructor to create a thin working copy such that readers of - * the original map do not notice new changes. Used for atomic - * changes with the Legacy API. See Providers::putAll. - */ - ServicesMapImpl(ServicesMapImpl original) { - services = new ConcurrentHashMap<>(original.services); - legacySvcKeys = original.legacySvcKeys; - serviceProps = original.serviceProps; - serviceAttrProps = original.serviceAttrProps; - } + /* + * Clears the internal ServicesMap state. The caller must synchronize + * changes with the Properties map. + */ + void clear() { + services.clear(); + legacySvcKeys.clear(); + serviceProps.clear(); + serviceAttrProps.clear(); + serviceSet.set(null); + } - /* - * Finds information about a service on the internal map. The key - * for the lookup can be either algorithm or alias based. If the - * service is found, svc refers to it, algKey to the algorithm - * service key and isLegacy informs if the service was stored with - * the Current or the Legacy API. Otherwise, svc is null, algKey - * refers to the key used for the lookup and isLegacy is null. - */ - private MappingInfo find(ServiceKey key) { - Service svc = services.get(key); - ServiceKey algKey = svc != null ? svc.algKey : key; - Boolean isLegacy = svc != null ? - legacySvcKeys.contains(algKey) : null; - return new MappingInfo(svc, algKey, isLegacy); - } + /* + * Finds information about a service on the internal map. The key for + * the lookup can be either algorithm or alias based. If the service is + * found, svc refers to it, algKey to the algorithm service key and + * isLegacy informs if the service was stored with the Current or the + * Legacy API. Otherwise, svc is null, algKey refers to the key used for + * the lookup and isLegacy is null. + */ + private MappingInfo find(ServiceKey key) { + Service svc = services.get(key); + ServiceKey algKey = svc != null ? svc.algKey : key; + Boolean isLegacy = svc != null ? + legacySvcKeys.contains(algKey) : null; + return new MappingInfo(svc, algKey, isLegacy); + } - /* - * Returns a set of services with services stored on the internal - * map. This method can be invoked concurrently with write accesses - * on the map and is lock-free. - */ - Set getServices() { - Set set = new LinkedHashSet<>(); - for (Map.Entry e : services.entrySet()) { - Service svc = e.getValue(); - // - // Skip alias based entries and filter out invalid services. - // - // Note: Multiple versions of the same service (reflecting - // different points in time) can be generated by concurrent - // writes with the Legacy API and, as a result of the - // copy-on-write strategy, seen under different service - // keys here. Each version has a unique object identity - // and, thus, would be distinguishable for a Set - // set. To avoid duplicates, we skip alias keys and use - // the version of the service pointed by the algorithm key. - if (e.getKey().equals(svc.algKey) && isValid(svc)) { - set.add(svc); - } - } - return set; - } + /* + * Signals that there were changes on the services map and the cached + * set of services need to be recomputed before use. + */ + private void notifyChanges() { + serviceSet.set(null); + } - Service getService(ServiceKey key) { - Service svc = services.get(key); - return svc != null && isValid(svc) ? svc : null; - } + /* + * A service is invalid if it was added with the Legacy API through an + * alias and does not have class information yet. We keep these services + * on the internal map but filter them out for readers, so they don't + * cause a NullPointerException when trying to create a new instance. + */ + private boolean isValid(Service svc) { + return svc.className != null; + } - void clear() { - services.clear(); - legacySvcKeys.clear(); - serviceProps.clear(); - serviceAttrProps.clear(); - } + /* + * Methods to add and remove services to the map according to the + * Current API. These methods update the Properties map to reflect + * service changes, including algorithms, aliases and attributes. + * + * Services added with the Legacy API may be overwritten with these + * methods. + * + * These methods guarantee atomicity from a service reader point of + * view. In other words, a reader that gets a service will see all its + * attributes and aliases as they were at time of registration. + */ - /* - * Signals that there were changes on the services map and the - * cached set of services need to be recomputed before use. - */ - private void notifyChanges() { - serviceSet.set(null); - } + boolean putService(Service svc) { + svc.generateServiceKeys(); - /* - * A service is invalid if it was added with the Legacy API through - * an alias and does not have class information yet. We keep these - * services on the internal map but filter them out for readers, so - * they don't cause a NullPointerException when trying to create a - * new instance. - */ - private boolean isValid(Service svc) { - return svc.className != null; - } + // Define a set of algorithm and alias keys that, if already on the + // services map, will be kept at all times until overwritten. This + // prevents concurrent readers from seeing 'holes' on the map while + // doing updates. + Set keysToBeKept = + new HashSet<>(svc.aliasKeys.size() + 1); + keysToBeKept.add(svc.algKey); + keysToBeKept.addAll(svc.aliasKeys.keySet()); - /* - * Current API methods to add and remove services. - */ + // The new service algorithm key may be in use already. + resolveKeyConflict(svc.algKey, keysToBeKept); - @Override - public SvcOpResult putService(Service svc) { - svc.generateServiceKeys(); + // The service will be registered to its provider's ServicesMap. + svc.registered = true; - // Define a set of algorithm and alias keys that, if already - // on the services map, will be kept at all times until - // overwritten. This prevents concurrent readers from seeing - // 'holes' on the map while doing updates. - Set keysToBeKept = - new HashSet<>(svc.aliasKeys.size() + 1); - keysToBeKept.add(svc.algKey); - keysToBeKept.addAll(svc.aliasKeys.keySet()); + // Register the new service under its algorithm service key. At this + // point, readers will have access to it. + services.put(svc.algKey, svc); - // The new service algorithm key may be in use already. - resolveKeyConflict(svc.algKey, keysToBeKept); + // Add an entry to the Properties map to reflect the new service + // under its algorithm key, and keep track of this information for + // further changes in the future (i.e. removal of the service). + String propKey = svc.getType() + "." + svc.getAlgorithm(); + serviceProps.put(svc.algKey, propKey); + Provider.super.put(propKey, svc.getClassName()); - // The service will be registered to its provider's ServicesMap. - svc.registered = true; + // Register the new service under its aliases. + for (Map.Entry e : svc.aliasKeys.entrySet()) { + ServiceKey aliasKey = e.getKey(); - // Register the new service under its algorithm service key. - // At this point, readers will have access to it. - services.put(svc.algKey, svc); + // The new service alias may be in use already. + resolveKeyConflict(aliasKey, keysToBeKept); - // Add an entry to the Properties map to reflect the new service - // under its algorithm key, and keep track of this information - // for further changes in the future (i.e. removal of the - // service). - String propKey = svc.getType() + "." + svc.getAlgorithm(); - serviceProps.put(svc.algKey, propKey); - Provider.super.put(propKey, svc.getClassName()); - - // Register the new service under its aliases. - for (Map.Entry e : - svc.aliasKeys.entrySet()) { - ServiceKey aliasKey = e.getKey(); - - // The new service alias may be in use already. - resolveKeyConflict(aliasKey, keysToBeKept); - - // Register the new service under its alias service key. At - // this point, readers will have access through this alias. - services.put(aliasKey, svc); - - // Add an entry to the Properties map to reflect the new - // service under its alias service key, and keep track - // of this information for further changes in the future - // (i.e. removal of the service). - propKey = ALIAS_PREFIX + svc.getType() + "." + e.getValue(); - serviceProps.put(aliasKey, propKey); - Provider.super.put(propKey, svc.getAlgorithm()); - } + // Register the new service under its alias service key. At this + // point, readers will have access through this alias. + services.put(aliasKey, svc); - if (!svc.attributes.isEmpty()) { - // Register the new service attributes on the Properties map - // and keep track of them for further changes in the future - // (i.e. removal of the service). - Map newAttrProps = - new HashMap<>(svc.attributes.size()); - for (Map.Entry attr : - svc.attributes.entrySet()) { - propKey = svc.getType() + "." + svc.getAlgorithm() + - " " + attr.getKey().string; - newAttrProps.put(attr.getKey(), propKey); - Provider.super.put(propKey, attr.getValue()); - } - serviceAttrProps.put(svc.algKey, newAttrProps); + // Add an entry to the Properties map to reflect the new service + // under its alias service key, and keep track of this + // information for further changes in the future (i.e. removal + // of the service). + propKey = ALIAS_PREFIX + svc.getType() + "." + e.getValue(); + serviceProps.put(aliasKey, propKey); + Provider.super.put(propKey, svc.getAlgorithm()); + } + + if (!svc.attributes.isEmpty()) { + // Register the new service attributes on the Properties map and + // keep track of them for further changes in the future (i.e. + // removal of the service). + Map newAttrProps = + new HashMap<>(svc.attributes.size()); + for (Map.Entry attr : + svc.attributes.entrySet()) { + propKey = svc.getType() + "." + svc.getAlgorithm() + " " + + attr.getKey().string; + newAttrProps.put(attr.getKey(), propKey); + Provider.super.put(propKey, attr.getValue()); } + serviceAttrProps.put(svc.algKey, newAttrProps); + } - Provider.this.checkAndUpdateSecureRandom(svc.algKey, true); + Provider.this.checkAndUpdateSecureRandom(svc.algKey, true); - return SvcOpResult.SUCCESS; - } + return true; + } - /* - * Handle cases in which a service key (algorithm or alias based) - * is in use already. This might require modifications to a service, - * the Properties map or auxiliary structures. This method must be - * called from the Current API only. - */ - private void resolveKeyConflict(ServiceKey key, - Set keysToBeKept) { - assert keysToBeKept.contains(key) : "Inconsistent " + - "keysToBeKept set."; - MappingInfo miByKey = find(key); - if (miByKey.svc != null) { - // The service key (algorithm or alias) is in use already. - SvcOpResult opResult = SvcOpResult.SUCCESS; - if (miByKey.algKey.equals(key)) { - // It is used as an algorithm. Remove the service. - opResult = removeCommon(miByKey, false, keysToBeKept); + /* + * Handle cases in which a service key (algorithm or alias based) is in + * use already. This might require modifications to a service, the + * Properties map or auxiliary structures. This method must be called + * from the Current API only. + */ + private void resolveKeyConflict(ServiceKey key, + Set keysToBeKept) { + assert keysToBeKept.contains(key) : + "Inconsistent keysToBeKept set."; + MappingInfo miByKey = find(key); + if (miByKey.svc != null) { + // The service key (algorithm or alias) is in use already. + boolean opSucceeded = true; + if (miByKey.algKey.equals(key)) { + // It is used as an algorithm. Remove the service. + opSucceeded = removeCommon(miByKey, false, keysToBeKept); + } else { + // It is used as an alias. + if (miByKey.isLegacy) { + // The service was added with the Legacy API. Remove the + // alias only. + opSucceeded = removeAliasLegacy(miByKey.algKey, key, + keysToBeKept); } else { - // It is used as an alias. - if (miByKey.isLegacy) { - // The service was added with the Legacy API. - // Remove the alias only. - opResult = removeAlias(miByKey.algKey, key, - keysToBeKept); - } else { - // The service was added with the Current API. - // Overwrite the alias entry on the services map - // without modifying the service that is currently - // using it. - - // Remove any Properties map key entry because, if - // no longer used as an alias, the entry would not - // be overwritten. Note: The serviceProps key entry - // will be overwritten later. - String oldPropKey = serviceProps.remove(key); - assert oldPropKey != null : - "Invalid alias property."; - Provider.super.remove(oldPropKey); - } + // The service was added with the Current API. Overwrite + // the alias entry on the services map without modifying + // the service that is currently using it. + + // Remove any Properties map key entry because, if no + // longer used as an alias, the entry would not be + // overwritten. Note: The serviceProps key entry will be + // overwritten later. + String oldPropKey = serviceProps.remove(key); + assert oldPropKey != null : "Invalid alias property."; + Provider.super.remove(oldPropKey); } - assert opResult == SvcOpResult.SUCCESS : "Unexpected" + - " error removing an existing service or alias."; } + assert opSucceeded : "Unexpected error removing an existing " + + "service or alias."; } + } - @Override - public SvcOpResult removeService(Service svc) { - if (svc.algKey != null) { - MappingInfo mi = find(svc.algKey); - if (mi.svc != null) { - SvcOpResult opResult = removeCommon(mi, false, - Collections.emptySet()); - assert opResult == SvcOpResult.SUCCESS : "Unexpected" + - " error removing an existing service."; - return opResult; - } + boolean removeService(Service svc) { + if (svc.algKey != null) { + MappingInfo mi = find(svc.algKey); + if (mi.svc != null) { + boolean opSucceeded = removeCommon(mi, false, + Collections.emptySet()); + assert opSucceeded : "Unexpected error removing an " + + "existing service."; + return opSucceeded; } - return SvcOpResult.SUCCESS; } + return true; + } - /* - * Common (Current and Legacy) API methods to add and remove - * services. - */ - - /* - * This method is invoked both when removing and overwriting a - * service. The keysToBeKept set is used when overwriting to - * prevent readers from seeing a 'hole' on the services map - * between removing and adding entries. - */ - private SvcOpResult removeCommon(MappingInfo mi, - boolean legacyApiCall, Set keysToBeKept) { - assert mi.svc != null : "Invalid service for removal."; - if (!mi.isLegacy && legacyApiCall) { - // Services added with the Current API cannot be - // removed with the Legacy API. - return SvcOpResult.ERROR; - } - - if (mi.isLegacy) { - legacySvcKeys.remove(mi.algKey); - } - - if (!keysToBeKept.contains(mi.algKey)) { - services.remove(mi.algKey); - } - - // Update the Properties map to reflect the algorithm removal. - // Note: oldPropKey may be null for services added through - // aliases or attributes (Legacy API) that still don't have a - // class name (invalid). - String oldPropKey = serviceProps.remove(mi.algKey); - if (oldPropKey != null) { - Provider.super.remove(oldPropKey); - } - - // Remove registered service aliases. - for (ServiceKey aliasKey : mi.svc.aliasKeys.keySet()) { - if (!mi.isLegacy) { - // Services added with the Current API can have aliases - // overwritten by other services added with the same - // API. Do nothing in these cases: the alias on the - // services map does not belong to the removed service - // anymore. - MappingInfo miByAlias = find(aliasKey); - if (miByAlias.svc != mi.svc) { - continue; - } - } - if (!keysToBeKept.contains(aliasKey)) { - services.remove(aliasKey); - } - - // Update the Properties map to reflect the alias removal. - // Note: oldPropKey cannot be null because aliases always - // have a corresponding Properties map entry. - oldPropKey = serviceProps.remove(aliasKey); - assert oldPropKey != null : "Unexpected null " + - "Property value for an alias."; - Provider.super.remove(oldPropKey); - } + /* + * Common (Current and Legacy) API methods to add and remove services. + */ - // Remove registered service attributes. - Map oldAttrProps = - serviceAttrProps.remove(mi.algKey); - if (oldAttrProps != null) { - for (String oldAttrPropKey : oldAttrProps.values()) { - // Update the Properties map to reflect the attribute - // removal. Note: oldAttrPropKey cannot be null because - // attributes always have a corresponding Properties map - // entry. - assert oldAttrPropKey != null : "Unexpected null " + - "Property value for an attribute."; - Provider.super.remove(oldAttrPropKey); - } - } + /* + * This method is invoked both when removing and overwriting a service. + * The keysToBeKept set is used when overwriting to prevent readers from + * seeing a 'hole' on the services map between removing and adding + * entries. + */ + private boolean removeCommon(MappingInfo mi, boolean legacyApiCall, + Set keysToBeKept) { + assert mi.svc != null : "Invalid service for removal."; + if (!mi.isLegacy && legacyApiCall) { + // Services added with the Current API cannot be removed with + // the Legacy API. + return false; + } - notifyChanges(); + if (mi.isLegacy) { + legacySvcKeys.remove(mi.algKey); + } - Provider.this.checkAndUpdateSecureRandom(mi.svc.algKey, false); + if (!keysToBeKept.contains(mi.algKey)) { + services.remove(mi.algKey); + } - return SvcOpResult.SUCCESS; + // Update the Properties map to reflect the algorithm removal. Note: + // oldPropKey may be null for services added through aliases or + // attributes (Legacy API) that still don't have a class name + // (invalid). + String oldPropKey = serviceProps.remove(mi.algKey); + if (oldPropKey != null) { + Provider.super.remove(oldPropKey); } - /* - * Legacy API methods to add, modify and remove services. - */ - - @Override - public SvcOpResult putClassName(ServiceKey key, String className, - String propKey) { - assert key != null && className != null && propKey != null : - "Service information missing."; - return updateSvc(key, (MappingInfo oldMi, Service newSvc) -> { - String canonicalPropKey = propKey; - if (oldMi.svc != null) { - // The service exists. Get its Properties map entry. - // Note: Services added through an alias or an attribute - // may don't have one. - String oldPropKey = serviceProps.get(oldMi.algKey); - if (oldMi.algKey.equals(key)) { - // The service was found by an algorithm. - if (oldPropKey != null) { - // Remove any previous Properties map entry - // before adding a new one, so we handle - // differences in casing. - Provider.super.remove(oldPropKey); - } - } else { - // The service was found by an alias. Use an - // algorithm entry on the Properties map. Create a - // new one if it does not exist. - canonicalPropKey = oldPropKey != null ? - oldPropKey : newSvc.getType() + "." + - newSvc.getAlgorithm(); - } + // Remove registered service aliases. + for (ServiceKey aliasKey : mi.svc.aliasKeys.keySet()) { + if (!mi.isLegacy) { + // Services added with the Current API can have aliases + // overwritten by other services added with the same API. Do + // nothing in these cases: the alias on the services map + // does not belong to the removed service anymore. + MappingInfo miByAlias = find(aliasKey); + if (miByAlias.svc != mi.svc) { + continue; } + } + if (!keysToBeKept.contains(aliasKey)) { + services.remove(aliasKey); + } - newSvc.className = className; - - // Keep track of the Properties map entry for further - // changes in the future (i.e. removal of the service). - serviceProps.put(oldMi.algKey, canonicalPropKey); - Provider.super.put(canonicalPropKey, className); - - Provider.this.checkAndUpdateSecureRandom( - newSvc.algKey, true); - - return SvcOpResult.SUCCESS; - }); + // Update the Properties map to reflect the alias removal. Note: + // oldPropKey cannot be null because aliases always have a + // corresponding Properties map entry. + oldPropKey = serviceProps.remove(aliasKey); + assert oldPropKey != null : + "Unexpected null Property value for an alias."; + Provider.super.remove(oldPropKey); + } + + // Remove registered service attributes. + Map oldAttrProps = + serviceAttrProps.remove(mi.algKey); + if (oldAttrProps != null) { + for (String oldAttrPropKey : oldAttrProps.values()) { + // Update the Properties map to reflect the attribute + // removal. Note: oldAttrPropKey cannot be null because + // attributes always have a corresponding Properties map + // entry. + assert oldAttrPropKey != null : + "Unexpected null Property value for an attribute."; + Provider.super.remove(oldAttrPropKey); + } } - @Override - public SvcOpResult putAlias(ServiceKey key, ServiceKey aliasKey, - String propKey) { - assert key != null && aliasKey != null && propKey != null : - "Alias information missing."; - assert key.type.equals(aliasKey.type) : - "Inconsistent service key types."; - return updateSvc(key, (MappingInfo oldMi, Service newSvc) -> { - MappingInfo miByAlias = find(aliasKey); - if (miByAlias.svc != null) { - // The alias is associated to a service on the map. - if (miByAlias.algKey.equals(aliasKey)) { - // The alias is an algorithm. Never overwrite - // algorithms with aliases from the Legacy API. - return SvcOpResult.ERROR; - } else if (!miByAlias.isLegacy) { - // Do not remove the alias of services added with - // the Current API. - return SvcOpResult.ERROR; - } else if (miByAlias.svc == oldMi.svc) { - // The service has the alias that we are adding. - // This is possible if, for example, the alias - // casing is changing. - // - // Update the Properties map to remove the alias - // with the old casing. Note: oldPropKey cannot be - // null because aliases always have a corresponding - // Properties map entry. - String oldPropKey = serviceProps.remove(aliasKey); - assert oldPropKey != null : "Unexpected null " + - "Property value for an alias."; - Provider.super.remove(oldPropKey); - } else { - // The alias belongs to a different service. - // Remove it first. - SvcOpResult opResult = removeAlias(miByAlias.algKey, - aliasKey, Set.of(aliasKey)); - assert opResult == SvcOpResult.SUCCESS : - "Unexpected error removing an alias."; - } - } else { - // The alias was not found on the map. - if (aliasKey.equals(key)) { - // The alias would be equal to the algorithm for - // the new service. - return SvcOpResult.ERROR; - } - } + notifyChanges(); - newSvc.addAliasKey(aliasKey); + Provider.this.checkAndUpdateSecureRandom(mi.svc.algKey, false); - // Keep track of the Properties map entry for further - // changes in the future (i.e. removal of the service). - serviceProps.put(aliasKey, propKey); - // If the service to which we will add an alias was found by - // an alias, use its algorithm for the Properties map entry. - String canonicalAlgorithm = oldMi.algKey.equals(key) ? - key.originalAlgorithm : newSvc.getAlgorithm(); - Provider.super.put(propKey, canonicalAlgorithm); + return true; + } - return SvcOpResult.SUCCESS; - }); - } + /* + * Methods to add, modify and remove services on the map according to + * the Legacy API. These methods update the Properties map to reflect + * service changes, including algorithms, aliases and attributes. + * + * Services added with the Current API cannot be overwritten with this + * API. + * + * Notice that these methods do not guarantee atomicity in a sequence of + * operations from a service reader point of view. As an example, a + * service reader may get a service missing an attribute if looked up + * between a writer's ServicesMap::putClassNameLegacy and + * ServicesMap::putAttributeLegacy calls. For atomic changes with the + * Legacy API see Provider::putAll. + */ - @Override - public SvcOpResult putAttribute(ServiceKey key, String attrName, - String attrValue, String propKey) { - assert key != null && attrName != null && attrValue != null && - propKey != null : "Attribute information missing."; - return updateSvc(key, (MappingInfo oldMi, Service newSvc) -> { - String canonicalPropKey = propKey; - UString attrNameKey = new UString(attrName); - Map attrProps = - serviceAttrProps.computeIfAbsent( - oldMi.algKey, k -> new HashMap<>()); - assert oldMi.svc != null || attrProps.isEmpty() : - "Inconsistent service attributes data."; - // Try to get the attribute's Properties map entry. Note: - // oldPropKey can be null if the service was not found or - // does not have the attribute. - String oldPropKey = attrProps.get(attrNameKey); + boolean putClassNameLegacy(ServiceKey key, String className, + String propKey) { + assert key != null && className != null && propKey != null : + "Service information missing."; + return updateSvc(key, (MappingInfo oldMi, Service newSvc) -> { + String canonicalPropKey = propKey; + if (oldMi.svc != null) { + // The service exists. Get its Properties map entry. Note: + // Services added through an alias or an attribute may don't + // have one. + String oldPropKey = serviceProps.get(oldMi.algKey); if (oldMi.algKey.equals(key)) { // The service was found by an algorithm. if (oldPropKey != null) { @@ -1249,276 +1135,283 @@ public SvcOpResult putAttribute(ServiceKey key, String attrName, } } else { // The service was found by an alias. Use an algorithm - // based entry on the Properties map. Create a new one - // if it does not exist. + // entry on the Properties map. Create a new one if it + // does not exist. canonicalPropKey = oldPropKey != null ? oldPropKey : - newSvc.getType() + "." + newSvc.getAlgorithm() + - " " + attrName; + newSvc.getType() + "." + newSvc.getAlgorithm(); } - - newSvc.addAttribute(attrName, attrValue); - - // Keep track of the Properties map entry for further - // changes in the future (i.e. removal of the service). - attrProps.put(attrNameKey, canonicalPropKey); - Provider.super.put(canonicalPropKey, attrValue); - - return SvcOpResult.SUCCESS; - }); - } - - @Override - public SvcOpResult remove(ServiceKey key, String className) { - assert key != null && className != null : - "Service information missing."; - MappingInfo mi = find(key); - if (mi.svc != null) { - assert className.equals(mi.svc.getClassName()) : - "Unexpected class name."; - return removeCommon(mi, true, Collections.emptySet()); } - assert false : "Should not reach."; - return SvcOpResult.ERROR; - } - @Override - public SvcOpResult removeAlias(ServiceKey key, - ServiceKey aliasKey) { - return removeAlias(key, aliasKey, Collections.emptySet()); - } + newSvc.className = className; - /* - * This method is invoked both when removing and overwriting a - * service alias. The keysToBeKept set is used when overwriting to - * prevent readers from seeing a 'hole' on the services map between - * removing and adding entries. - */ - private SvcOpResult removeAlias(ServiceKey key, ServiceKey aliasKey, - Set keysToBeKept) { - assert key != null && aliasKey != null && keysToBeKept != null : - "Alias information missing."; - return updateSvc(key, (MappingInfo oldMi, Service newSvc) -> { - MappingInfo miByAlias = find(aliasKey); - if (oldMi.svc != null && miByAlias.svc == oldMi.svc && - !miByAlias.algKey.equals(aliasKey)) { - // The alias is a real alias and is associated to the - // service on the map. - if (!keysToBeKept.contains(aliasKey)) { - services.remove(aliasKey); - } + // Keep track of the Properties map entry for further changes in + // the future (i.e. removal of the service). + serviceProps.put(oldMi.algKey, canonicalPropKey); + Provider.super.put(canonicalPropKey, className); - newSvc.removeAliasKey(aliasKey); + Provider.this.checkAndUpdateSecureRandom(newSvc.algKey, true); - // Update the Properties map to reflect the alias - // removal. Note: oldPropKey cannot be null because - // aliases always have a corresponding Properties map - // entry. + return true; + }); + } + + boolean putAliasLegacy(ServiceKey key, ServiceKey aliasKey, + String propKey) { + assert key != null && aliasKey != null && propKey != null : + "Alias information missing."; + assert key.type.equals(aliasKey.type) : + "Inconsistent service key types."; + return updateSvc(key, (MappingInfo oldMi, Service newSvc) -> { + MappingInfo miByAlias = find(aliasKey); + if (miByAlias.svc != null) { + // The alias is associated to a service on the map. + if (miByAlias.algKey.equals(aliasKey)) { + // The alias is an algorithm. Never overwrite algorithms + // with aliases from the Legacy API. + return false; + } else if (!miByAlias.isLegacy) { + // Do not remove the alias of services added with the + // Current API. + return false; + } else if (miByAlias.svc == oldMi.svc) { + // The service has the alias that we are adding. This is + // possible if, for example, the alias casing is + // changing. + // + // Update the Properties map to remove the alias with + // the old casing. Note: oldPropKey cannot be null + // because aliases always have a corresponding + // Properties map entry. String oldPropKey = serviceProps.remove(aliasKey); - assert oldPropKey != null : "Invalid alias property."; - Provider.super.remove(oldPropKey); - - return SvcOpResult.SUCCESS; - } - assert false : "Should not reach."; - return SvcOpResult.ERROR; - }); - } - - @Override - public SvcOpResult removeAttribute(ServiceKey key, - String attrName, String attrValue) { - assert key != null && attrName != null && attrValue != null : - "Attribute information missing."; - return updateSvc(key, (MappingInfo oldMi, Service newSvc) -> { - Map oldAttrProps = - serviceAttrProps.get(oldMi.algKey); - if (oldAttrProps != null) { - // The service was found and has attributes. - assert oldMi.svc != null : "Inconsistent service " + - "attributes data."; - - newSvc.removeAttribute(attrName, attrValue); - assert newSvc.getAttribute(attrName) == null : - "Attribute was not removed from the service."; - - // Update the Properties map to reflect the attribute - // removal. Note: oldPropKey cannot be null because - // attributes always have a corresponding Properties - // map entry. - String oldPropKey = oldAttrProps.remove( - new UString(attrName)); assert oldPropKey != null : - "Invalid attribute property."; + "Unexpected null Property value for an alias."; Provider.super.remove(oldPropKey); - - if (oldAttrProps.isEmpty()) { - // If the removed attribute was the last one, - // remove the map. - serviceAttrProps.remove(oldMi.algKey); - } - - return SvcOpResult.SUCCESS; - } - assert false : "Should not reach."; - return SvcOpResult.ERROR; - }); - } - - @FunctionalInterface - private interface ServiceUpdateCallback { - SvcOpResult apply(MappingInfo oldMi, Service newSvc); - } - - /* - * This method tries to find a service on the map (based on an - * algorithm or alias) and pass a copy of it to an update callback - * (copy-on-write). If the service found was added with the Current - * API, no update should be done. If a service was not found, a new - * instance may be created. - * - * The updated version of the service is put on the services map. - * Algorithm and alias based entries pointing to the old version - * of the service are overwritten. - */ - private SvcOpResult updateSvc(ServiceKey key, - ServiceUpdateCallback updateCb) { - Service newSvc; - MappingInfo oldMi = find(key); - if (oldMi.svc != null) { - // Service exists. - if (!oldMi.isLegacy) { - // Don't update services added with the Current API. - return SvcOpResult.ERROR; + } else { + // The alias belongs to a different service. Remove it + // first. + boolean opSucceeded = removeAliasLegacy( + miByAlias.algKey, aliasKey, Set.of(aliasKey)); + assert opSucceeded : + "Unexpected error removing an alias."; } - // Create a copy of the service for a copy-on-write update. - newSvc = new Service(oldMi.svc); } else { - // Service does not exist. - newSvc = new Service(Provider.this, key); - } - SvcOpResult opResult = updateCb.apply(oldMi, newSvc); - if (opResult == SvcOpResult.ERROR) { - // Something went wrong and the update should not be done. - return opResult; + // The alias was not found on the map. + if (aliasKey.equals(key)) { + // The alias would be equal to the algorithm for the new + // service. + return false; + } } - // The service (or its updated version) will be registered to - // its provider's ServicesMap. - newSvc.registered = true; + newSvc.addAliasKey(aliasKey); + + // Keep track of the Properties map entry for further changes in + // the future (i.e. removal of the service). + serviceProps.put(aliasKey, propKey); + // If the service to which we will add an alias was found by an + // alias, use its algorithm for the Properties map entry. + String canonicalAlgorithm = oldMi.algKey.equals(key) ? + key.originalAlgorithm : newSvc.getAlgorithm(); + Provider.super.put(propKey, canonicalAlgorithm); - // Register the updated version of the service under its - // algorithm and aliases on the map. This may overwrite entries - // or add new ones. The previous callback should have handled - // the removal of an alias. - for (ServiceKey aliasKey : newSvc.aliasKeys.keySet()) { - services.put(aliasKey, newSvc); + return true; + }); + } + + boolean putAttributeLegacy(ServiceKey key, String attrName, + String attrValue, String propKey) { + assert key != null && attrName != null && attrValue != null && + propKey != null : "Attribute information missing."; + return updateSvc(key, (MappingInfo oldMi, Service newSvc) -> { + String canonicalPropKey = propKey; + UString attrNameKey = new UString(attrName); + Map attrProps = + serviceAttrProps.computeIfAbsent( + oldMi.algKey, k -> new HashMap<>()); + assert oldMi.svc != null || attrProps.isEmpty() : + "Inconsistent service attributes data."; + // Try to get the attribute's Properties map entry. Note: + // oldPropKey can be null if the service was not found or does + // not have the attribute. + String oldPropKey = attrProps.get(attrNameKey); + if (oldMi.algKey.equals(key)) { + // The service was found by an algorithm. + if (oldPropKey != null) { + // Remove any previous Properties map entry before + // adding a new one, so we handle differences in casing. + Provider.super.remove(oldPropKey); + } + } else { + // The service was found by an alias. Use an algorithm based + // entry on the Properties map. Create a new one if it does + // not exist. + canonicalPropKey = oldPropKey != null ? oldPropKey : + newSvc.getType() + "." + newSvc.getAlgorithm() + + " " + attrName; } - assert oldMi.algKey.type.equals(newSvc.getType()) && - oldMi.algKey.originalAlgorithm.equals( - newSvc.getAlgorithm()) : "Invalid key."; - services.put(oldMi.algKey, newSvc); + newSvc.addAttribute(attrName, attrValue); - legacySvcKeys.add(oldMi.algKey); + // Keep track of the Properties map entry for further changes in + // the future (i.e. removal of the service). + attrProps.put(attrNameKey, canonicalPropKey); + Provider.super.put(canonicalPropKey, attrValue); - // Notify a change. - notifyChanges(); + return true; + }); + } - return opResult; + boolean removeLegacy(ServiceKey key, String className) { + assert key != null && className != null : + "Service information missing."; + MappingInfo mi = find(key); + if (mi.svc != null) { + assert className.equals(mi.svc.getClassName()) : + "Unexpected class name."; + return removeCommon(mi, true, Collections.emptySet()); } + assert false : "Should not reach."; + return false; } - // Placeholder for a thread to mark that serviceSet values are being - // computed after a services update. Only one thread at a time can - // effectively assign this value. - private static final Set SERVICE_SET_IN_PROGRESS = Set.of(); - - // Unmodifiable set of all services. Possible values for this field - // are: 1) null (indicates that the set has to be recomputed after a - // service update), 2) SERVICE_SET_IN_PROGRESS (indicates that a thread - // is recomputing its value), and 3) an actual set of services. - private final AtomicReference> serviceSet; - - // Implementation of ServicesMap that handles the Current and Legacy - // APIs. - private final ServicesMapImpl impl; - - ServicesMap() { - impl = new ServicesMapImpl(); - serviceSet = new AtomicReference<>(); + boolean removeAliasLegacy(ServiceKey key, ServiceKey aliasKey) { + return removeAliasLegacy(key, aliasKey, Collections.emptySet()); } /* - * Constructor to create a thin working copy such that readers of the - * original map do not notice any new changes. Used for atomic - * changes with the Legacy API. See Providers::putAll. + * This method is invoked both when removing and overwriting a service + * alias. The keysToBeKept set is used when overwriting to prevent + * readers from seeing a 'hole' on the services map between removing and + * adding entries. */ - ServicesMap(ServicesMap original) { - impl = new ServicesMapImpl(original.impl); - serviceSet = new AtomicReference<>(original.serviceSet.get()); + private boolean removeAliasLegacy(ServiceKey key, ServiceKey aliasKey, + Set keysToBeKept) { + assert key != null && aliasKey != null && keysToBeKept != null : + "Alias information missing."; + return updateSvc(key, (MappingInfo oldMi, Service newSvc) -> { + MappingInfo miByAlias = find(aliasKey); + if (oldMi.svc != null && miByAlias.svc == oldMi.svc && + !miByAlias.algKey.equals(aliasKey)) { + // The alias is a real alias and is associated to the + // service on the map. + if (!keysToBeKept.contains(aliasKey)) { + services.remove(aliasKey); + } + + newSvc.removeAliasKey(aliasKey); + + // Update the Properties map to reflect the alias removal. + // Note: oldPropKey cannot be null because aliases always + // have a corresponding Properties map entry. + String oldPropKey = serviceProps.remove(aliasKey); + assert oldPropKey != null : "Invalid alias property."; + Provider.super.remove(oldPropKey); + + return true; + } + assert false : "Should not reach."; + return false; + }); } - /* - * Returns a Current API view of the services map. - */ - Current asCurrent() { - return impl; + boolean removeAttributeLegacy(ServiceKey key, String attrName, + String attrValue) { + assert key != null && attrName != null && attrValue != null : + "Attribute information missing."; + return updateSvc(key, (MappingInfo oldMi, Service newSvc) -> { + Map oldAttrProps = + serviceAttrProps.get(oldMi.algKey); + if (oldAttrProps != null) { + // The service was found and has attributes. + assert oldMi.svc != null : + "Inconsistent service attributes data."; + + newSvc.removeAttribute(attrName, attrValue); + assert newSvc.getAttribute(attrName) == null : + "Attribute was not removed from the service."; + + // Update the Properties map to reflect the attribute + // removal. Note: oldPropKey cannot be null because + // attributes always have a corresponding Properties map + // entry. + String oldPropKey = oldAttrProps.remove( + new UString(attrName)); + assert oldPropKey != null : "Invalid attribute property."; + Provider.super.remove(oldPropKey); + + if (oldAttrProps.isEmpty()) { + // If the removed attribute was the last one, remove the + // map. + serviceAttrProps.remove(oldMi.algKey); + } + + return true; + } + assert false : "Should not reach."; + return false; + }); } - /* - * Returns a Legacy API view of the services map. - */ - Legacy asLegacy() { - return impl; + @FunctionalInterface + private interface ServiceUpdateCallback { + boolean apply(MappingInfo oldMi, Service newSvc); } /* - * Returns an unmodifiable set of available services. Recomputes - * serviceSet if needed, after a service update. Both services added - * with the Current and Legacy APIs are included. If no services are - * found, the returned set is empty. This method is thread-safe and - * lock-free. + * This method tries to find a service on the map (based on an + * algorithm or alias) and pass a copy of it to an update callback + * (copy-on-write). If the service found was added with the Current API, + * no update should be done. If a service was not found, a new instance + * may be created. + * + * The updated version of the service is put on the services map. + * Algorithm and alias based entries pointing to the old version of the + * service are overwritten. */ - Set getServices() { - Set serviceSetLocal = serviceSet.compareAndExchange( - null, SERVICE_SET_IN_PROGRESS); - if (serviceSetLocal == null || - serviceSetLocal == SERVICE_SET_IN_PROGRESS) { - // A cached set is not available. Instead of locking, compute - // the set to be returned and, eventually, make it available - // for others to use. - Set newSet = Collections.unmodifiableSet( - impl.getServices()); - if (serviceSetLocal == null) { - // We won the race to make the computed set available for - // others to use. However, only make it available if it - // is still current (in other words, there were no further - // changes). If it is not current, the next reader will - // do the job. - serviceSet.compareAndExchange( - SERVICE_SET_IN_PROGRESS, newSet); + private boolean updateSvc(ServiceKey key, + ServiceUpdateCallback updateCb) { + Service newSvc; + MappingInfo oldMi = find(key); + if (oldMi.svc != null) { + // Service exists. + if (!oldMi.isLegacy) { + // Don't update services added with the Current API. + return false; } - serviceSetLocal = newSet; + // Create a copy of the service for a copy-on-write update. + newSvc = new Service(oldMi.svc); + } else { + // Service does not exist. + newSvc = new Service(Provider.this, key); + } + if (!updateCb.apply(oldMi, newSvc)) { + // Something went wrong and the update should not be done. + return false; } - return serviceSetLocal; - } - /* - * Returns an available service. Both services added with the Current - * and Legacy APIs are considered in the search. Thread-safe and - * lock-free. - */ - Service getService(ServiceKey key) { - return impl.getService(key); - } + // The service (or its updated version) will be registered to its + // provider's ServicesMap. + newSvc.registered = true; - /* - * Clears the internal ServicesMap state. The caller must synchronize - * changes with the Properties map. - */ - void clear() { - impl.clear(); - serviceSet.set(null); + // Register the updated version of the service under its algorithm + // and aliases on the map. This may overwrite entries or add new + // ones. The previous callback should have handled the removal of an + // alias. + for (ServiceKey aliasKey : newSvc.aliasKeys.keySet()) { + services.put(aliasKey, newSvc); + } + + assert oldMi.algKey.type.equals(newSvc.getType()) && + oldMi.algKey.originalAlgorithm.equals( + newSvc.getAlgorithm()) : "Invalid key."; + services.put(oldMi.algKey, newSvc); + + legacySvcKeys.add(oldMi.algKey); + + // Notify a change. + notifyChanges(); + + return true; } } @@ -1836,10 +1729,9 @@ private PropertiesMapAction parseLegacy(ServicesMap servicesMap, ServiceKey svcKey = new ServiceKey(type, propValue, true); ServiceKey aliasKey = new ServiceKey(type, aliasAlg, true); switch (opType) { - case ADD -> servicesMap.asLegacy() - .putAlias(svcKey, aliasKey, propKey); - case REMOVE -> servicesMap.asLegacy() - .removeAlias(svcKey, aliasKey); + case ADD -> servicesMap.putAliasLegacy(svcKey, aliasKey, + propKey); + case REMOVE -> servicesMap.removeAliasLegacy(svcKey, aliasKey); } } else { String[] typeAndAlg = getTypeAndAlgorithm(propKey); @@ -1854,10 +1746,9 @@ private PropertiesMapAction parseLegacy(ServicesMap servicesMap, String algo = typeAndAlg[1].intern(); ServiceKey svcKey = new ServiceKey(type, algo, true); switch (opType) { - case ADD -> servicesMap.asLegacy() - .putClassName(svcKey, propValue, propKey); - case REMOVE -> servicesMap.asLegacy() - .remove(svcKey, propValue); + case ADD -> servicesMap.putClassNameLegacy(svcKey, propValue, + propKey); + case REMOVE -> servicesMap.removeLegacy(svcKey, propValue); } } else { // attribute // e.g. put("MessageDigest.SHA-1 ImplementedIn", "Software"); @@ -1872,10 +1763,10 @@ private PropertiesMapAction parseLegacy(ServicesMap servicesMap, attrName = attrName.intern(); ServiceKey svcKey = new ServiceKey(type, algo, true); switch (opType) { - case ADD -> servicesMap.asLegacy() - .putAttribute(svcKey, attrName, propValue, propKey); - case REMOVE -> servicesMap.asLegacy() - .removeAttribute(svcKey, attrName, propValue); + case ADD -> servicesMap.putAttributeLegacy(svcKey, attrName, + propValue, propKey); + case REMOVE -> servicesMap.removeAttributeLegacy(svcKey, + attrName, propValue); } } } @@ -1970,7 +1861,7 @@ protected synchronized void putService(Service s) { throw new IllegalArgumentException ("service.getProvider() must match this Provider object"); } - servicesMap.asCurrent().putService(s); + servicesMap.putService(s); } private void checkAndUpdateSecureRandom(ServiceKey algKey, boolean doAdd) { @@ -2023,7 +1914,7 @@ protected synchronized void removeService(Service s) { if (s == null) { throw new NullPointerException(); } - servicesMap.asCurrent().removeService(s); + servicesMap.removeService(s); } // Wrapped String that behaves in a case-insensitive way for equals/hashCode @@ -2358,7 +2249,7 @@ public Service(Provider provider, String type, String algorithm, /* * When a Service is added to a ServicesMap with the Current API, * service and alias keys must be generated. Currently used by - * ServicesMapImpl::putService. Legacy API methods do not need to call: + * ServicesMap::putService. Legacy API methods do not need to call: * they generated the algorithm key at construction time and alias * keys with Service::addAliasKey. */ From 3d0df51b634e02e95ee66c4121460e8fe6b9d9be Mon Sep 17 00:00:00 2001 From: Francisco Ferrari Bihurriet Date: Wed, 12 Feb 2025 17:24:19 -0300 Subject: [PATCH 4/7] Clear ServicesMap fields in the declared order Constructors assign the fields in the same order. --- src/java.base/share/classes/java/security/Provider.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/java.base/share/classes/java/security/Provider.java b/src/java.base/share/classes/java/security/Provider.java index 455a0cbb9bd31..3e5ded4697f24 100644 --- a/src/java.base/share/classes/java/security/Provider.java +++ b/src/java.base/share/classes/java/security/Provider.java @@ -834,11 +834,11 @@ Service getService(ServiceKey key) { * changes with the Properties map. */ void clear() { + serviceSet.set(null); services.clear(); legacySvcKeys.clear(); serviceProps.clear(); serviceAttrProps.clear(); - serviceSet.set(null); } /* From 413cd7f549f5db908fb68004d773aa9704c6ecf9 Mon Sep 17 00:00:00 2001 From: Martin Balao Date: Wed, 2 Apr 2025 21:10:53 -0400 Subject: [PATCH 5/7] Assorted minor changes. Co-authored-by: Martin Balao Alonso Co-authored-by: Francisco Ferrari Bihurriet --- .../share/classes/java/security/Provider.java | 98 ++++++++++--------- 1 file changed, 54 insertions(+), 44 deletions(-) diff --git a/src/java.base/share/classes/java/security/Provider.java b/src/java.base/share/classes/java/security/Provider.java index 3e5ded4697f24..67a36382a371a 100644 --- a/src/java.base/share/classes/java/security/Provider.java +++ b/src/java.base/share/classes/java/security/Provider.java @@ -731,8 +731,9 @@ private record MappingInfo(Service svc, ServiceKey algKey, // Auxiliary set to determine if a service on the services map was added // with the Legacy API. The absence of a service key on this set is an - // indication that the service was either not added or added with the - // Current API. Only algorithm service keys are added to this set. + // indication that the service was either not added to the services + // map (any API) or was added with the Current API. Only algorithm + // service keys are added to this set. private final Set legacySvcKeys; // Auxiliary map to keep track of the Properties map entries that @@ -747,6 +748,9 @@ private record MappingInfo(Service svc, ServiceKey algKey, // added to this map. private final Map> serviceAttrProps; + /* + * Default constructor. Creates an empty services map for a Provider. + */ ServicesMap() { serviceSet = new AtomicReference<>(); services = new ConcurrentHashMap<>(); @@ -983,9 +987,10 @@ private void resolveKeyConflict(ServiceKey key, opSucceeded = removeAliasLegacy(miByKey.algKey, key, keysToBeKept); } else { - // The service was added with the Current API. Overwrite - // the alias entry on the services map without modifying - // the service that is currently using it. + // The service was added with the Current API. Once we + // return to putService, the alias entry on the services + // map will be overwritten without modifying the service + // that is currently associated with. // Remove any Properties map key entry because, if no // longer used as an alias, the entry would not be @@ -1122,7 +1127,7 @@ boolean putClassNameLegacy(ServiceKey key, String className, String canonicalPropKey = propKey; if (oldMi.svc != null) { // The service exists. Get its Properties map entry. Note: - // Services added through an alias or an attribute may don't + // Services added through an alias or an attribute may not // have one. String oldPropKey = serviceProps.get(oldMi.algKey); if (oldMi.algKey.equals(key)) { @@ -1367,6 +1372,8 @@ private interface ServiceUpdateCallback { * The updated version of the service is put on the services map. * Algorithm and alias based entries pointing to the old version of the * service are overwritten. + * + * This method is invoked from the Legacy API only. */ private boolean updateSvc(ServiceKey key, ServiceUpdateCallback updateCb) { @@ -1533,9 +1540,9 @@ private void implClear() { } private Object implRemove(Object key) { - Object oldValue = super.get(key); - return doLegacyOp(servicesMap, key, oldValue, null, OPType.REMOVE) == - PropertiesMapAction.UPDATE ? super.remove(key) : oldValue; + Object value = super.get(key); + return doLegacyOp(servicesMap, key, value, null, OPType.REMOVE) == + PropertiesMapAction.UPDATE ? super.remove(key) : value; } private boolean implRemove(Object key, Object value) { @@ -1651,7 +1658,6 @@ private Object implPut(ServicesMap servicesMap, Object key, Object value) { PropertiesMapAction.UPDATE ? super.put(key, value) : oldValue; } - // used as key in the serviceMap and legacyMap HashMaps private static final class ServiceKey { private final String type; private final String algorithm; @@ -2066,16 +2072,19 @@ public static class Service { private Map attributes; private final EngineDescription engineDescription; - // For services added to a ServicesMap, their algorithm service key. - // This value derives from the algorithm field. For services (still) - // not added to a ServicesMap, value is null. + // This value is the algorithm service key when the service is added to + // the ServicesMap. Value is null otherwise. private ServiceKey algKey; - // For services added to a ServicesMap, this is a map from alias service - // keys to alias string values. Empty map if no aliases. While map - // entries derive from the aliases field, keys are not repeated - // (case-insensitive comparison) and not equal to the algorithm. For - // services (still) not added to a ServicesMap, value is an empty map. + // This map is empty if the service has no aliases or has not been + // registered in the ServicesMap. When the service has aliases and is + // registered in the ServicesMap, this information maps the alias + // ServiceKey to the string alias as originally defined in the service. + // + // The purpose for this map is both to keep references to alias + // ServiceKeys for reuse (i.e. we don't have to construct them from + // strings multiple times) and to facilitate alias string updates with + // different casing from the Legacy API. private Map aliasKeys; // Reference to the cached implementation Class object. @@ -2107,7 +2116,7 @@ public static class Service { private static final Class[] CLASS0 = new Class[0]; /* - * Constructor used from the ServicesMap Legacy API. + * Constructor used by the ServicesMap Legacy API. */ private Service(Provider provider, ServiceKey algKey) { assert algKey.algorithm.intern() == algKey.algorithm : @@ -2146,7 +2155,6 @@ private Service(Service svc) { } else { attributes = new HashMap<>(svc.attributes); } - registered = false; // Do not copy cached fields because the updated service may have a // different class name or attributes and these values have to be @@ -2238,7 +2246,7 @@ public Service(Provider provider, String type, String algorithm, if (attributes == null) { this.attributes = Collections.emptyMap(); } else { - this.attributes = new HashMap<>(); + this.attributes = new HashMap<>(attributes.size(), 1.0f); for (Map.Entry entry : attributes.entrySet()) { this.attributes.put(new UString(entry.getKey()), entry.getValue()); @@ -2246,29 +2254,6 @@ public Service(Provider provider, String type, String algorithm, } } - /* - * When a Service is added to a ServicesMap with the Current API, - * service and alias keys must be generated. Currently used by - * ServicesMap::putService. Legacy API methods do not need to call: - * they generated the algorithm key at construction time and alias - * keys with Service::addAliasKey. - */ - private void generateServiceKeys() { - if (algKey == null) { - assert (Object)aliasKeys == Collections.emptyMap() : - "aliasKeys expected to be the empty map."; - algKey = new ServiceKey(type, algorithm, true); - aliasKeys = new HashMap<>(aliases.size()); - for (String alias : aliases) { - ServiceKey aliasKey = new ServiceKey(type, alias, true); - if (!aliasKey.equals(algKey)) { - aliasKeys.put(aliasKey, alias); - } - } - aliasKeys = Collections.unmodifiableMap(aliasKeys); - } - } - /** * Get the type of this service. For example, {@code MessageDigest}. * @@ -2404,6 +2389,31 @@ public Object newInstance(Object constructorParameter) } } + /* + * When a Service is added to a ServicesMap with the Current API, + * service and alias keys must be generated. Currently used by + * ServicesMap::putService. Legacy API methods do not need to call: + * they generated the algorithm key at construction time and alias + * keys with Service::addAliasKey. + */ + private void generateServiceKeys() { + if (algKey == null) { + assert (Object)aliasKeys == Collections.emptyMap() : + "aliasKeys expected to be the empty map."; + algKey = new ServiceKey(type, algorithm, true); + if (!aliases.isEmpty()) { + aliasKeys = new HashMap<>(aliases.size()); + for (String alias : aliases) { + ServiceKey aliasKey = new ServiceKey(type, alias, true); + if (!aliasKey.equals(algKey)) { + aliasKeys.put(aliasKey, alias); + } + } + aliasKeys = Collections.unmodifiableMap(aliasKeys); + } + } + } + private Object newInstanceOf() throws Exception { Constructor con = getDefaultConstructor(); return con.newInstance(EMPTY); From 384f2a7bcde604bb71e2279455095c05d1bede9f Mon Sep 17 00:00:00 2001 From: Martin Balao Date: Thu, 3 Apr 2025 18:01:27 -0400 Subject: [PATCH 6/7] Minor doc change missing. Co-authored-by: Martin Balao Alonso Co-authored-by: Francisco Ferrari Bihurriet --- src/java.base/share/classes/java/security/Provider.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/java.base/share/classes/java/security/Provider.java b/src/java.base/share/classes/java/security/Provider.java index 67a36382a371a..ea835368b64ad 100644 --- a/src/java.base/share/classes/java/security/Provider.java +++ b/src/java.base/share/classes/java/security/Provider.java @@ -2132,7 +2132,7 @@ private Service(Provider provider, ServiceKey algKey) { } /* - * Copy constructor used from the ServicesMap Legacy API for the + * Copy constructor used by the ServicesMap Legacy API for the * copy-on-write strategy. This constructor is invoked after every * update to a service on the ServicesMap. */ @@ -2167,7 +2167,7 @@ private Service(Service svc) { } /* - * Methods used from the ServicesMap Legacy API to update a service. + * Methods used by the ServicesMap Legacy API to update a service. */ private void addAliasKey(ServiceKey aliasKey) { From da693b12c8aaa794b6574400627c63d6cd53a2a7 Mon Sep 17 00:00:00 2001 From: Martin Balao Date: Thu, 3 Apr 2025 18:09:45 -0400 Subject: [PATCH 7/7] Sync of methods reading the properties map. Co-authored-by: Martin Balao Alonso Co-authored-by: Francisco Ferrari Bihurriet --- src/java.base/share/classes/java/security/Provider.java | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/java.base/share/classes/java/security/Provider.java b/src/java.base/share/classes/java/security/Provider.java index ea835368b64ad..c72d9a1ccc693 100644 --- a/src/java.base/share/classes/java/security/Provider.java +++ b/src/java.base/share/classes/java/security/Provider.java @@ -429,7 +429,7 @@ public synchronized Set> entrySet() { * @since 1.2 */ @Override - public Set keySet() { + public synchronized Set keySet() { checkInitialized(); return Collections.unmodifiableSet(super.keySet()); } @@ -441,7 +441,7 @@ public Set keySet() { * @since 1.2 */ @Override - public Collection values() { + public synchronized Collection values() { checkInitialized(); return Collections.unmodifiableCollection(super.values()); } @@ -672,7 +672,8 @@ public synchronized Enumeration elements() { } // let javadoc show doc from superclass - public String getProperty(String key) { + @Override + public synchronized String getProperty(String key) { checkInitialized(); return super.getProperty(key); }