diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/osgi/BundleHost.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/osgi/BundleHost.java index 5e1b57d316..18903f7a4a 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/osgi/BundleHost.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/osgi/BundleHost.java @@ -22,7 +22,6 @@ import java.util.*; import java.util.Map.Entry; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.CountDownLatch; -import java.util.function.Function; import java.util.stream.Collectors; import org.apache.felix.framework.FrameworkFactory; @@ -63,8 +62,7 @@ public class BundleHost { private static final String SAVE_STATE_TAG_ACTIVE = "BundleHost_ACTIVE"; private static final String SAVE_STATE_TAG_SYSTEM = "BundleHost_SYSTEM"; - Map fileToBundleMap = new HashMap<>(); - Map bundleLocationToBundleMap = new HashMap<>(); + private final BundleMap bundleMap = new BundleMap(); BundleContext frameworkBundleContext; Framework felixFramework; @@ -93,7 +91,7 @@ public class BundleHost { * @return false if the bundle was already enabled */ public boolean enable(ResourceFile bundleFile) { - GhidraBundle bundle = fileToBundleMap.get(bundleFile); + GhidraBundle bundle = bundleMap.get(bundleFile); if (bundle == null) { bundle = add(bundleFile, true, false); return true; @@ -139,7 +137,7 @@ public class BundleHost { * @return a {@link GhidraBundle} or {@code null} */ public GhidraBundle getExistingGhidraBundle(ResourceFile bundleFile) { - GhidraBundle bundle = fileToBundleMap.get(bundleFile); + GhidraBundle bundle = bundleMap.get(bundleFile); if (bundle == null) { Msg.showError(this, null, "ghidra bundle cache", "getExistingGhidraBundle expected a GhidraBundle created at " + bundleFile + @@ -156,7 +154,7 @@ public class BundleHost { * @return a {@link GhidraBundle} or {@code null} */ public GhidraBundle getGhidraBundle(ResourceFile bundleFile) { - return fileToBundleMap.get(bundleFile); + return bundleMap.get(bundleFile); } private static GhidraBundle createGhidraBundle(BundleHost bundleHost, ResourceFile bundleFile, @@ -189,25 +187,11 @@ public class BundleHost { */ public GhidraBundle add(ResourceFile bundleFile, boolean enabled, boolean systemBundle) { GhidraBundle bundle = createGhidraBundle(this, bundleFile, enabled, systemBundle); - fileToBundleMap.put(bundleFile, bundle); - bundleLocationToBundleMap.put(bundle.getLocationIdentifier(), bundle); + bundleMap.add(bundle); fireBundleAdded(bundle); return bundle; } - private Set dedupeBundleFiles(List bundleFiles) { - Set dedupedBundleFiles = new HashSet<>(bundleFiles); - Iterator bundleFileIterator = dedupedBundleFiles.iterator(); - while (bundleFileIterator.hasNext()) { - ResourceFile bundleFile = bundleFileIterator.next(); - if (fileToBundleMap.containsKey(bundleFile)) { - bundleFileIterator.remove(); - Msg.warn(this, "adding an already managed bundle: " + bundleFile.getAbsolutePath()); - } - } - return dedupedBundleFiles; - } - /** * Create new GhidraBundles and add to the list of managed bundles. All GhidraBundles created * with the same {@code enabled} and {@code systemBundle} values. @@ -219,17 +203,8 @@ public class BundleHost { */ public Collection add(List bundleFiles, boolean enabled, boolean systemBundle) { - Set dedupedBundleFiles = dedupeBundleFiles(bundleFiles); - Map newBundleMap = dedupedBundleFiles.stream() - .collect(Collectors.toUnmodifiableMap(Function.identity(), - bundleFile -> createGhidraBundle(BundleHost.this, bundleFile, enabled, - systemBundle))); - fileToBundleMap.putAll(newBundleMap); - bundleLocationToBundleMap.putAll(newBundleMap.values() - .stream() - .collect(Collectors.toUnmodifiableMap(GhidraBundle::getLocationIdentifier, - Function.identity()))); - Collection newBundles = newBundleMap.values(); + Collection newBundles = bundleMap.computeAllIfAbsent(bundleFiles, + bundleFile -> createGhidraBundle(BundleHost.this, bundleFile, enabled, systemBundle)); fireBundlesAdded(newBundles); return newBundles; } @@ -240,10 +215,7 @@ public class BundleHost { * @param bundles the bundles to add */ private void add(List bundles) { - for (GhidraBundle bundle : bundles) { - fileToBundleMap.put(bundle.getFile(), bundle); - bundleLocationToBundleMap.put(bundle.getLocationIdentifier(), bundle); - } + bundleMap.addAll(bundles); fireBundlesAdded(bundles); } @@ -253,8 +225,7 @@ public class BundleHost { * @param bundleFile the file of the bundle to remove */ public void remove(ResourceFile bundleFile) { - GhidraBundle bundle = fileToBundleMap.remove(bundleFile); - bundleLocationToBundleMap.remove(bundle.getLocationIdentifier()); + GhidraBundle bundle = bundleMap.remove(bundleFile); fireBundleRemoved(bundle); } @@ -264,8 +235,7 @@ public class BundleHost { * @param bundleLocation the location id of the bundle to remove */ public void remove(String bundleLocation) { - GhidraBundle bundle = bundleLocationToBundleMap.remove(bundleLocation); - fileToBundleMap.remove(bundle.getFile()); + GhidraBundle bundle = bundleMap.remove(bundleLocation); fireBundleRemoved(bundle); } @@ -275,8 +245,7 @@ public class BundleHost { * @param bundle the bundle to remove */ public void remove(GhidraBundle bundle) { - fileToBundleMap.remove(bundle.getFile()); - bundleLocationToBundleMap.remove(bundle.getLocationIdentifier()); + bundleMap.remove(bundle); fireBundleRemoved(bundle); } @@ -286,10 +255,7 @@ public class BundleHost { * @param bundles the bundles to remove */ public void remove(Collection bundles) { - for (GhidraBundle bundle : bundles) { - fileToBundleMap.remove(bundle.getFile()); - bundleLocationToBundleMap.remove(bundle.getLocationIdentifier()); - } + bundleMap.removeAll(bundles); fireBundlesRemoved(bundles); } @@ -338,7 +304,7 @@ public class BundleHost { * @return all the bundles */ public Collection getGhidraBundles() { - return fileToBundleMap.values(); + return bundleMap.getGhidraBundles(); } /** @@ -347,7 +313,7 @@ public class BundleHost { * @return all the bundle files */ public Collection getBundleFiles() { - return fileToBundleMap.keySet(); + return bundleMap.getBundleFiles(); } void dumpLoadedBundles() { @@ -861,7 +827,7 @@ public class BundleHost { boolean isEnabled = bundleIsEnabled[i]; boolean isActive = bundleIsActive[i]; boolean isSystem = bundleIsSystem[i]; - GhidraBundle bundle = fileToBundleMap.get(bundleFile); + GhidraBundle bundle = bundleMap.get(bundleFile); if (bundle != null) { if (isEnabled != bundle.isEnabled()) { bundle.setEnabled(isEnabled); @@ -900,14 +866,15 @@ public class BundleHost { * @param saveState the state object */ public void saveManagedBundleState(SaveState saveState) { - int numBundles = fileToBundleMap.size(); + Collection bundles = bundleMap.getGhidraBundles(); + int numBundles = bundles.size(); String[] bundleFiles = new String[numBundles]; boolean[] bundleIsEnabled = new boolean[numBundles]; boolean[] bundleIsActive = new boolean[numBundles]; boolean[] bundleIsSystem = new boolean[numBundles]; int index = 0; - for (GhidraBundle bundle : fileToBundleMap.values()) { + for (GhidraBundle bundle : bundles) { bundleFiles[index] = generic.util.Path.toPathString(bundle.getFile()); bundleIsEnabled[index] = bundle.isEnabled(); bundleIsActive[index] = bundle.isActive(); @@ -1081,7 +1048,7 @@ public class BundleHost { GhidraBundle bundle; switch (event.getType()) { case BundleEvent.STARTED: - bundle = bundleLocationToBundleMap.get(osgiBundle.getLocation()); + bundle = bundleMap.getBundleAtLocation(osgiBundle.getLocation()); if (bundle != null) { fireBundleActivationChange(bundle, true); } @@ -1093,7 +1060,7 @@ public class BundleHost { break; // force "inactive" updates for all other states default: - bundle = bundleLocationToBundleMap.get(osgiBundle.getLocation()); + bundle = bundleMap.getBundleAtLocation(osgiBundle.getLocation()); if (bundle != null) { fireBundleActivationChange(bundle, false); } diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/osgi/BundleMap.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/osgi/BundleMap.java new file mode 100644 index 0000000000..cee4bf6519 --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/osgi/BundleMap.java @@ -0,0 +1,225 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ghidra.app.plugin.core.osgi; + +import java.util.*; +import java.util.concurrent.locks.*; +import java.util.function.Function; +import java.util.stream.Collectors; + +import generic.jar.ResourceFile; + +/** + * A thread-safe container that maps {@link GhidraBundle}s by file and bundle location. + */ +public class BundleMap { + private final ReadWriteLock lock = new ReentrantReadWriteLock(); + private final Lock readLock = lock.readLock(); + private final Lock writeLock = lock.writeLock(); + + private final Map bundlesByFile = new HashMap<>(); + private final Map bundlesByLocation = new HashMap<>(); + + /** + * Maps associations between a bundle, its file, and its bundle location. + * + * @param bundle a GhidraBundle object + */ + public void add(GhidraBundle bundle) { + writeLock.lock(); + try { + bundlesByFile.put(bundle.getFile(), bundle); + bundlesByLocation.put(bundle.getLocationIdentifier(), bundle); + } + finally { + lock.writeLock().unlock(); + } + } + + /** + * Maps bundles in a collection. + * + *

This is the same as calling {@link BundleMap#add(GhidraBundle)} for each bundle in {@code bundles}. + * + * @param bundles a collection of GhidraBundle objects + */ + public void addAll(Collection bundles) { + writeLock.lock(); + try { + for (GhidraBundle bundle : bundles) { + bundlesByFile.put(bundle.getFile(), bundle); + bundlesByLocation.put(bundle.getLocationIdentifier(), bundle); + } + } + finally { + writeLock.unlock(); + } + } + + /** + * Removes the mappings of a bundle. + * + * @param bundle a GhidraBundle object + */ + public void remove(GhidraBundle bundle) { + writeLock.lock(); + try { + bundlesByFile.remove(bundle.getFile()); + bundlesByLocation.remove(bundle.getLocationIdentifier()); + } + finally { + writeLock.unlock(); + } + } + + /** + * Removes all mappings of each bundle from a collection. + * + * This is the same as calling {@link #remove(GhidraBundle)} for each bundle in {@code bundles}. + * + * @param bundles a collection of GhidraBundle objects + */ + public void removeAll(Collection bundles) { + writeLock.lock(); + try { + for (GhidraBundle bundle : bundles) { + bundlesByFile.remove(bundle.getFile()); + bundlesByLocation.remove(bundle.getLocationIdentifier()); + } + } + finally { + writeLock.unlock(); + } + } + + /** + * Removes the mapping for a bundle with a given bundle location. + * + * @param bundleLocation a bundle location + * @return the bundle removed + */ + public GhidraBundle remove(String bundleLocation) { + writeLock.lock(); + try { + GhidraBundle bundle = bundlesByLocation.remove(bundleLocation); + bundlesByFile.remove(bundle.getFile()); + return bundle; + } + finally { + writeLock.unlock(); + } + } + + /** + * Removes the mapping for a bundle with a given file. + + * @param bundleFile a bundle file + * @return the bundle removed + */ + public GhidraBundle remove(ResourceFile bundleFile) { + writeLock.lock(); + try { + GhidraBundle bundle = bundlesByFile.remove(bundleFile); + bundlesByLocation.remove(bundle.getLocationIdentifier()); + return bundle; + } + finally { + writeLock.unlock(); + } + } + + /** + * Creates and maps bundles from files in a collection that aren't already mapped. + * + * @param bundleFiles a collection of bundle files + * @param ctor a constructor for a GhidraBundle given a bundle file + * @return the newly created GhidraBundle objects + */ + public Collection computeAllIfAbsent(Collection bundleFiles, + Function ctor) { + writeLock.lock(); + try { + Set newBundleFiles = new HashSet<>(bundleFiles); + newBundleFiles.removeAll(bundlesByFile.keySet()); + List newBundles = + newBundleFiles.stream().map(ctor).collect(Collectors.toList()); + addAll(newBundles); + return newBundles; + } + finally { + writeLock.unlock(); + } + } + + /** + * Returns the bundle with the given location. + * + * @param location a bundle location + * @return the bundle found or null + */ + public GhidraBundle getBundleAtLocation(String location) { + readLock.lock(); + try { + return bundlesByLocation.get(location); + } + finally { + readLock.unlock(); + } + } + + /** + * Returns the bundle with the given file. + * + * @param bundleFile a bundle file + * @return the bundle found or null + */ + public GhidraBundle get(ResourceFile bundleFile) { + readLock.lock(); + try { + return bundlesByFile.get(bundleFile); + } + finally { + readLock.unlock(); + } + } + + /** + * @return the currently mapped bundles + */ + public Collection getGhidraBundles() { + readLock.lock(); + try { + return new ArrayList<>(bundlesByFile.values()); + } + finally { + readLock.unlock(); + } + } + + /** + * @return the currently mapped bundle files + */ + public Collection getBundleFiles() { + readLock.lock(); + try { + return new ArrayList<>(bundlesByFile.keySet()); + } + finally { + readLock.unlock(); + } + } + +}