aboutsummaryrefslogtreecommitdiffstats
path: root/component/src/main/java/com/yahoo/component/provider/ComponentRegistry.java
blob: a64d6eb1090519a72e64b05a1bd1be9b1aa204a3 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
package com.yahoo.component.provider;

import com.google.common.collect.ImmutableMap;
import com.yahoo.component.ComponentId;
import com.yahoo.component.ComponentSpecification;
import com.yahoo.component.Version;
import com.yahoo.component.VersionSpecification;

import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;

/**
 * A generic superclass for component registries. Supports registration and lookup
 * of components by id. The registry resolves id requests to the newest matching
 * component version registered.
 * <p>
 * This registry supports the <i>freeze</i> pattern - changes can be made
 * to this registry until {@link #freeze} is called. Subsequent change attempts will cause an
 * exception. Freezing a registry after building makes it possible to avoid locking and memory
 * synchronization on lookups.
 *
 * @author bratseth
 */
public class ComponentRegistry<COMPONENT> {

    /** All versions of all components, indexed by name and namespace */
    private final Map<ComponentId, Map<String, Map<Version, COMPONENT>>> componentsByNameByNamespace = new LinkedHashMap<>();

    /** All versions of all components indexed by id */
    private final Map<ComponentId, COMPONENT> componentsById =new LinkedHashMap<>();

    /** True when this cannot be changed any more */
    private boolean frozen = false;

    /**
     * Freezes this registry to prevent further changes. Override this to freeze internal data
     * structures and dependent objects. Overrides must call super.
     * Calling freeze on an already frozen registry must have no effect.
     */
    public synchronized void freeze() { frozen=true; }

    /** returns whether this is currently frozen */
    public final boolean isFrozen() { return frozen; }

    /**
     * Registers a component unless this registry is frozen.
     * This will succeed even if this component name and version is already registered.
     *
     * @throws IllegalStateException if this chain is frozen
     */
    public void register(ComponentId id, COMPONENT component) {
        if (frozen) throw new IllegalStateException("Cannot modify a frozen component registry");

        Map<String, Map<Version, COMPONENT>> componentVersionsByName =
                componentsByNameByNamespace.computeIfAbsent(id.getNamespace(), k -> new LinkedHashMap<>());

        Map<Version, COMPONENT> componentVersions = componentVersionsByName.computeIfAbsent(id.getName(), k -> new LinkedHashMap<>());
        componentVersions.put(id.getVersion(), component);

        componentsById.put(id, component);
    }


    /**
     * Unregisters a component unless this registry is frozen.
     * Note that the component is not deconstructed or otherwise modified in any way, this
     * is the responsiblity of the caller.
     *
     * @param id the id of the component to be unregistered
     * @return the component that was unregistered, or null if no such component was already registered
     */
    public COMPONENT unregister(ComponentId id) {
        if (frozen) throw new IllegalStateException("Cannot modify a frozen component registry");

        COMPONENT removed = componentsById.remove(id);

        if (removed != null) {
            //removed is non-null, so it must be present here as well:
            Map<String, Map<Version, COMPONENT>> componentVersionsByName = componentsByNameByNamespace.get(id.getNamespace());
            Map<Version, COMPONENT> componentVersions = componentVersionsByName.get(id.getName());
            COMPONENT removedInner = componentVersions.remove(id.getVersion());
            assert (removedInner == removed);

            //clean up
            if (componentVersions.isEmpty()) {
                componentVersionsByName.remove(id.getName());
            }
            if (componentVersionsByName.isEmpty()) {
                componentsByNameByNamespace.remove(id.getNamespace());
            }
        }
        return removed;
    }

    /**
     * See getComponent(ComponentSpecification)
     * @param  componentSpecification a component specification string, see {@link com.yahoo.component.Version}
     * @return the component or null if no component of this name (and version, if specified) is registered here
     */
    public COMPONENT getComponent(String componentSpecification) {
        return getComponent(new ComponentSpecification(componentSpecification));
    }

    public COMPONENT getComponent(ComponentId id) {
        return componentsById.get(id);
    }


    /**
     * Returns a component. If the id does not specify an (exact) version, the newest (matching) version is returned.
     * For example, if version 3.1 is specified and we have 3.1.0, 3.1.1 and 3.1.3 registered, 3.1.3 is returned.
     *
     * @param id the id of the component to return. May not include a version, or include
     *        an underspecified version, in which case the highest (matching) version which
     *        does not contain a qualifier is returned
     * @return the search chain or null if no component of this name (and matching version, if specified) is registered
     */
    public COMPONENT getComponent(ComponentSpecification id) {
        Map<String, Map<Version, COMPONENT>> componentVersionsByName = componentsByNameByNamespace.get(id.getNamespace());
        if (componentVersionsByName == null) return null;  // No matching namespace

        Map<Version, COMPONENT> versions = componentVersionsByName.get(id.getName());
        if (versions==null) return null; // No versions of this component

        Version version=findBestMatch(id.getVersionSpecification(), versions.keySet());
        //if (version==null) return null; // No matching version

        return versions.get(version);
    }

    /**
     * Finds the best (highest) matching version among a set.
     *
     * @return the matching version, or null if there are no matches
     */
    protected static Version findBestMatch(VersionSpecification versionSpec, Set<Version> versions) {
        Version bestMatch=null;
        for (Version version : versions) {
            //No version is set if getSpecifiedMajor() == null
            //In that case we allow all versions
            if (version == null || !versionSpec.matches(version)) continue;

            if (bestMatch==null || bestMatch.compareTo(version)<0)
                bestMatch=version;
        }
        return bestMatch;
    }

    /**
     * Returns an unmodifiable snapshot of all components present in this registry.
     */
    public List<COMPONENT> allComponents() {
        return List.copyOf(componentsById.values());
    }

    /**
     * Returns an unmodifiable snapshot of all components present in this registry, by id.
     */
    public Map<ComponentId, COMPONENT> allComponentsById() {
        return ImmutableMap.copyOf(componentsById);
    }

    /** Returns the number of components in this */
    public int getComponentCount() { return componentsById.size(); }

    /** Returns a frozen registry with a single component, for convenience */
    public static <COMPONENT> ComponentRegistry<COMPONENT> singleton(ComponentId id, COMPONENT component) {
        ComponentRegistry<COMPONENT> registry = new ComponentRegistry<>();
        registry.register(id, component);
        registry.freeze();
        return registry;
    }

}