summaryrefslogtreecommitdiffstats
path: root/jdisc_core/src/main/java/com/yahoo/jdisc/core/BundleCollisionHook.java
blob: 58ad5df9b0d09e08b1cd6acc64444cb566e1efea (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
package com.yahoo.jdisc.core;

import org.osgi.framework.Bundle;
import org.osgi.framework.BundleContext;
import org.osgi.framework.BundleEvent;
import org.osgi.framework.ServiceRegistration;
import org.osgi.framework.Version;
import org.osgi.framework.hooks.bundle.CollisionHook;
import org.osgi.framework.hooks.bundle.EventHook;
import org.osgi.framework.hooks.bundle.FindHook;

import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Objects;
import java.util.Set;

/**
 * A bundle {@link CollisionHook} that contains a set of bundles that are allowed to collide with
 * bundles that are about to be installed. In order to clean up when bundles are uninstalled, this
 * is also a bundle {@link EventHook}.
 *
 * Thread safe
 *
 * @author gjoranv
 */
public class BundleCollisionHook implements CollisionHook, EventHook, FindHook {

    private ServiceRegistration<?> registration;
    private Map<Bundle, BsnVersion> allowedDuplicates = new HashMap<>(5);

    public void start(BundleContext context) {
        if (registration != null) {
            throw new IllegalStateException();
        }
        String[] serviceClasses = {CollisionHook.class.getName(), EventHook.class.getName(), FindHook.class.getName()};
        registration = context.registerService(serviceClasses, this, null);
    }

    public void stop() {
        registration.unregister();
        registration = null;
    }

    /**
     * Adds a collection of bundles to the allowed duplicates.
     */
    synchronized void allowDuplicateBundles(Collection<Bundle> bundles) {
        for (var bundle : bundles) {
            allowedDuplicates.put(bundle, new BsnVersion(bundle));
        }
    }

    /**
     * Cleans up the allowed duplicates when a bundle is uninstalled.
     */
    @Override
    public void event(BundleEvent event, Collection<BundleContext> contexts) {
        if (event.getType() != BundleEvent.UNINSTALLED) return;

        synchronized (this) {
            allowedDuplicates.remove(event.getBundle());
        }
    }

    /**
     * Removes duplicates of the allowed duplicate bundles from the given collision candidates.
     */
    @Override
    public synchronized void filterCollisions(int operationType, Bundle target, Collection<Bundle> collisionCandidates) {
        Set<Bundle> whitelistedCandidates = new HashSet<>();
        for (var bundle : collisionCandidates) {
            // This is O(n), but n should be small here, plus this is only called when bundles collide.
            if (allowedDuplicates.containsValue(new BsnVersion(bundle))) {
                whitelistedCandidates.add(bundle);
            }
        }
        collisionCandidates.removeAll(whitelistedCandidates);
    }

    /**
     * Filters out the set of bundles that should not be visible to the bundle associated with the given context.
     * If the given context represents one of the allowed duplicates, this method filters out all bundles
     * that are duplicates of the allowed duplicates. Otherwise this method filters out the allowed duplicates,
     * so they are not visible to other bundles.
     *
     * NOTE:  This hook method is added for a consistent view of the installed bundles, but is not actively
     *        used by jdisc. The OSGi framework does not use FindHooks when calculating bundle wiring.
     */
    @Override
    public synchronized void find(BundleContext context, Collection<Bundle> bundles) {
        Set<Bundle> bundlesToHide = new HashSet<>();
        if (allowedDuplicates.containsKey(context.getBundle())) {
            for (var bundle : bundles) {
                // isDuplicate... is O(n), but n should be small here, plus this is only run for duplicate bundles.
                if (isDuplicateOfAllowedDuplicates(bundle)) {
                    bundlesToHide.add(bundle);
                }
            }
        } else {
            for (var bundle : bundles) {
                if (allowedDuplicates.containsKey(bundle)) {
                    bundlesToHide.add(bundle);
                }
            }
        }
        bundles.removeAll(bundlesToHide);
    }

    private boolean isDuplicateOfAllowedDuplicates(Bundle bundle) {
        return ! allowedDuplicates.containsKey(bundle) && allowedDuplicates.containsValue(new BsnVersion(bundle));
    }


    static class BsnVersion {

        private final String symbolicName;
        private final Version version;

        BsnVersion(Bundle bundle) {
            this.symbolicName = bundle.getSymbolicName();
            this.version = bundle.getVersion();
        }

        @Override
        public boolean equals(Object o) {
            if (this == o) return true;
            if (o == null || getClass() != o.getClass()) return false;
            BsnVersion that = (BsnVersion) o;
            return Objects.equals(symbolicName, that.symbolicName) &&
                    version.equals(that.version);
        }

        @Override
        public int hashCode() {
            return Objects.hash(symbolicName, version);
        }

    }

}