exclude mvn: and classpath: url bundles from persistence, and related tweaks

- change {white,black}list to include/exclude list, for bundles
- add ability to exclude bundles based on url prefix, now defaulting to mvn: and classpath:
- fix bug where setPersistenceNeeded never cleared it, so it persisted more often than it should
- also prevent persistence when rebinding, as there is no point
diff --git a/camp/camp-brooklyn/src/test/java/org/apache/brooklyn/camp/brooklyn/catalog/CatalogOsgiVersionMoreEntityRebindTest.java b/camp/camp-brooklyn/src/test/java/org/apache/brooklyn/camp/brooklyn/catalog/CatalogOsgiVersionMoreEntityRebindTest.java
index 29b33f3..a710d52 100644
--- a/camp/camp-brooklyn/src/test/java/org/apache/brooklyn/camp/brooklyn/catalog/CatalogOsgiVersionMoreEntityRebindTest.java
+++ b/camp/camp-brooklyn/src/test/java/org/apache/brooklyn/camp/brooklyn/catalog/CatalogOsgiVersionMoreEntityRebindTest.java
@@ -18,11 +18,12 @@
  */
 package org.apache.brooklyn.camp.brooklyn.catalog;
 
-import org.apache.brooklyn.util.exceptions.CompoundRuntimeException;
-import static org.testng.Assert.assertEquals;
-
 import java.util.Map;
 
+import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Iterables;
 import org.apache.brooklyn.api.effector.Effector;
 import org.apache.brooklyn.api.entity.Entity;
 import org.apache.brooklyn.api.entity.EntitySpec;
@@ -50,6 +51,7 @@
 import org.apache.brooklyn.util.collections.MutableMap;
 import org.apache.brooklyn.util.core.ClassLoaderUtils;
 import org.apache.brooklyn.util.core.ResourceUtils;
+import org.apache.brooklyn.util.exceptions.CompoundRuntimeException;
 import org.apache.brooklyn.util.exceptions.ReferenceWithError;
 import org.apache.brooklyn.util.javalang.Reflections;
 import org.apache.brooklyn.util.osgi.OsgiTestResources;
@@ -61,10 +63,7 @@
 import org.testng.Assert;
 import org.testng.annotations.Test;
 
-import com.google.common.base.Preconditions;
-import com.google.common.collect.ImmutableList;
-import com.google.common.collect.ImmutableMap;
-import com.google.common.collect.Iterables;
+import static org.testng.Assert.assertEquals;
 
 /** Many of the same tests as per {@link OsgiVersionMoreEntityTest} but using YAML for catalog and entities, so catalog item ID is set automatically */
 public class CatalogOsgiVersionMoreEntityRebindTest extends AbstractYamlRebindTest implements OsgiTestResources {
diff --git a/core/src/main/java/org/apache/brooklyn/core/catalog/internal/CatalogInitialization.java b/core/src/main/java/org/apache/brooklyn/core/catalog/internal/CatalogInitialization.java
index 7b24cec..5050977 100644
--- a/core/src/main/java/org/apache/brooklyn/core/catalog/internal/CatalogInitialization.java
+++ b/core/src/main/java/org/apache/brooklyn/core/catalog/internal/CatalogInitialization.java
@@ -555,6 +555,7 @@
         Set<OsgiBundleInstallationResult> bundlesToRemove = MutableSet.of();
         installs.values().stream().forEach(candidate -> {
             if (filteredPersistedState.getBundles().containsKey(candidate.getVersionedName())) {
+                candidate.setRebinding(true);
                 bundlesInOrder.add(candidate);
             } else {
                 log.debug("Skipping start of persisted bundle "+candidate+" due to catalog upgrade metadata instructions");
diff --git a/core/src/main/java/org/apache/brooklyn/core/mgmt/ha/BrooklynBomOsgiArchiveInstaller.java b/core/src/main/java/org/apache/brooklyn/core/mgmt/ha/BrooklynBomOsgiArchiveInstaller.java
index 237249d..cf47f23 100644
--- a/core/src/main/java/org/apache/brooklyn/core/mgmt/ha/BrooklynBomOsgiArchiveInstaller.java
+++ b/core/src/main/java/org/apache/brooklyn/core/mgmt/ha/BrooklynBomOsgiArchiveInstaller.java
@@ -27,26 +27,25 @@
 import java.io.*;
 import java.net.URL;
 import java.util.*;
+import java.util.function.BiConsumer;
 import java.util.function.Predicate;
 import java.util.function.Supplier;
 import java.util.jar.Attributes;
 import java.util.jar.Manifest;
-import java.util.regex.Pattern;
 import java.util.stream.Collectors;
 import java.util.zip.ZipEntry;
 import java.util.zip.ZipFile;
 import javax.annotation.Nullable;
 import org.apache.brooklyn.api.mgmt.ManagementContext;
+import org.apache.brooklyn.api.mgmt.rebind.ChangeListener;
+import org.apache.brooklyn.api.objs.BrooklynObject;
 import org.apache.brooklyn.api.typereg.ManagedBundle;
-import org.apache.brooklyn.api.typereg.OsgiBundleWithUrl;
 import org.apache.brooklyn.api.typereg.RegisteredType;
-import org.apache.brooklyn.config.ConfigKey;
 import org.apache.brooklyn.core.BrooklynVersion;
 import org.apache.brooklyn.core.catalog.internal.BasicBrooklynCatalog;
 import org.apache.brooklyn.core.catalog.internal.CatalogInitialization;
 import org.apache.brooklyn.core.mgmt.ha.OsgiBundleInstallationResult.ResultCode;
 import org.apache.brooklyn.core.mgmt.internal.ManagementContextInternal;
-import org.apache.brooklyn.core.server.BrooklynServerConfig;
 import org.apache.brooklyn.core.typereg.*;
 import org.apache.brooklyn.core.typereg.BundleUpgradeParser.CatalogUpgrades;
 import org.apache.brooklyn.util.collections.MutableList;
@@ -76,9 +75,6 @@
 
     private static final Logger log = LoggerFactory.getLogger(BrooklynBomOsgiArchiveInstaller.class);
     
-    public static final ConfigKey<String> PERSIST_MANAGED_BUNDLE_WHITELIST_REGEX = BrooklynServerConfig.PERSIST_MANAGED_BUNDLE_WHITELIST_REGEX;
-    public static final ConfigKey<String> PERSIST_MANAGED_BUNDLE_BLACKLIST_REGEX = BrooklynServerConfig.PERSIST_MANAGED_BUNDLE_BLACKLIST_REGEX;
-
     final private OsgiManager osgiManager;
     private ManagedBundle suppliedKnownBundleMetadata;
     private InputStream zipIn;
@@ -117,8 +113,6 @@
     private ManagedBundle inferredMetadata;
     private final boolean inputStreamSupplied;
     
-    private volatile Predicate<ManagedBundle> blacklistBundlePersistencePredicate;
-    
     public BrooklynBomOsgiArchiveInstaller(OsgiManager osgiManager, ManagedBundle knownBundleMetadata, InputStream zipIn) {
         this.osgiManager = osgiManager;
         this.suppliedKnownBundleMetadata = knownBundleMetadata;
@@ -709,11 +703,12 @@
                             log.error("Error rolling back following failed install of updated "+result.getVersionedName()+"; "
                                 + "installation will likely be corrupted and correct version should be manually installed.", e);
                         }
-                        
-                        if (!isBlacklistedForPersistence(result.getMetadata())) {
-                            ((BasicManagedBundle)result.getMetadata()).setPersistenceNeeded(true);
-                            mgmt().getRebindManager().getChangeListener().onChanged(result.getMetadata());
+
+                        if (!isExcludedFromPersistence(oldManagedBundle)) {
+                            ((BasicManagedBundle)oldManagedBundle).setPersistenceNeeded(true);
+                            mgmt().getRebindManager().getChangeListener().onChanged(oldManagedBundle);
                         }
+
                     } else {
                         if (isBringingExistingOsgiInstalledBundleUnderBrooklynManagement) {
                             log.debug("Uninstalling bundle "+result.getVersionedName()+" from Brooklyn management only (rollback needed but it was already installed to OSGi)");
@@ -721,7 +716,7 @@
                             log.debug("Uninstalling bundle "+result.getVersionedName()+" (roll back of failed fresh install, no previous version to revert to)");
                         }                        
                         osgiManager.uninstallUploadedBundle(result.getMetadata(), false, isBringingExistingOsgiInstalledBundleUnderBrooklynManagement);
-                        if (!isBlacklistedForPersistence(result.getMetadata())) {
+                        if (!isExcludedFromPersistence(result.getMetadata())) {
                             ((BasicManagedBundle)result.getMetadata()).setPersistenceNeeded(true);
                             mgmt().getRebindManager().getChangeListener().onUnmanaged(result.getMetadata());
                         }
@@ -731,7 +726,7 @@
                     if (start) {
                         try {
                             log.debug("Starting bundle "+result.getVersionedName());
-                            if (!isBlacklistedForPersistence(result.getMetadata())) {
+                            if (!isExcludedFromPersistence(result.getMetadata()) && !Boolean.TRUE.equals(result.rebinding)) {
                                 ((BasicManagedBundle)result.getMetadata()).setPersistenceNeeded(true);
                                 if (updating) {
                                     mgmt().getRebindManager().getChangeListener().onChanged(result.getMetadata());
@@ -914,36 +909,8 @@
     }
 
     @VisibleForTesting
-    boolean isBlacklistedForPersistence(ManagedBundle managedBundle) {
-        // We treat as "managed bundles" (to extract their catalog.bom) the contents of:
-        //   - org.apache.brooklyn.core
-        //   - org.apache.brooklyn.policy
-        //   - org.apache.brooklyn.test-framework
-        //   - org.apache.brooklyn.software-*
-        //   - org.apache.brooklyn.library-catalog
-        //   - org.apache.brooklyn.karaf-init (not sure why this one could end up in persisted state!)
-        // We don't want to persist the entire brooklyn distro! Therefore default is to blacklist those.
-        
-        if (blacklistBundlePersistencePredicate == null) {
-            String whitelistRegex = mgmt().getConfig().getConfig(PERSIST_MANAGED_BUNDLE_WHITELIST_REGEX);
-            String blacklistRegex = mgmt().getConfig().getConfig(PERSIST_MANAGED_BUNDLE_BLACKLIST_REGEX);
-            
-            final Pattern whitelistPattern = (whitelistRegex != null) ? Pattern.compile(whitelistRegex) : null;
-            final Pattern blacklistPattern = (blacklistRegex != null) ? Pattern.compile(blacklistRegex) : null;
-
-            blacklistBundlePersistencePredicate = input -> {
-                    String bundleName = input.getSymbolicName();
-                    if (whitelistPattern != null && whitelistPattern.matcher(bundleName).matches()) {
-                        return false;
-                    }
-                    if (blacklistPattern != null && blacklistPattern.matcher(bundleName).matches()) {
-                        return true;
-                    }
-                    return false;
-            };
-        }
-        
-        return blacklistBundlePersistencePredicate.test(managedBundle);
+    boolean isExcludedFromPersistence(ManagedBundle managedBundle) {
+        return osgiManager.isExcludedFromPersistence(managedBundle);
     }
     
     private static List<Bundle> findBundlesBySymbolicNameAndVersion(OsgiManager osgiManager, ManagedBundle desired) {
diff --git a/core/src/main/java/org/apache/brooklyn/core/mgmt/ha/OsgiBundleInstallationResult.java b/core/src/main/java/org/apache/brooklyn/core/mgmt/ha/OsgiBundleInstallationResult.java
index dbb48c4..f6c16b3 100644
--- a/core/src/main/java/org/apache/brooklyn/core/mgmt/ha/OsgiBundleInstallationResult.java
+++ b/core/src/main/java/org/apache/brooklyn/core/mgmt/ha/OsgiBundleInstallationResult.java
@@ -37,6 +37,7 @@
     Bundle bundle;
     ResultCode code;
     Runnable deferredStart;
+    Boolean rebinding;
     
     public enum ResultCode { 
         INSTALLED_NEW_BUNDLE(false),
@@ -117,4 +118,8 @@
         typesInstalled.add(ci);
         catalogItemsInstalled.add(ci.getId());        
     }
+
+    public void setRebinding(Boolean rebinding) {
+        this.rebinding = rebinding;
+    }
 }
\ No newline at end of file
diff --git a/core/src/main/java/org/apache/brooklyn/core/mgmt/ha/OsgiManager.java b/core/src/main/java/org/apache/brooklyn/core/mgmt/ha/OsgiManager.java
index 32588c6..4455028 100644
--- a/core/src/main/java/org/apache/brooklyn/core/mgmt/ha/OsgiManager.java
+++ b/core/src/main/java/org/apache/brooklyn/core/mgmt/ha/OsgiManager.java
@@ -18,24 +18,34 @@
  */
 package org.apache.brooklyn.core.mgmt.ha;
 
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URL;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Enumeration;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.Callable;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.function.Predicate;
+import java.util.function.Supplier;
+import java.util.regex.Pattern;
+import javax.annotation.Nullable;
+
 import com.google.common.annotations.Beta;
 import com.google.common.annotations.VisibleForTesting;
 import com.google.common.base.Optional;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.Iterables;
-import com.google.common.collect.Sets;
-import java.io.*;
-import java.net.URL;
-import java.util.*;
-import java.util.concurrent.Callable;
-import java.util.concurrent.Executor;
-import java.util.concurrent.ExecutorService;
-import java.util.concurrent.ThreadPoolExecutor;
-import java.util.concurrent.atomic.AtomicInteger;
-import java.util.concurrent.atomic.AtomicReference;
-import java.util.function.Supplier;
-import javax.annotation.Nullable;
 import org.apache.brooklyn.api.catalog.CatalogItem.CatalogBundle;
 import org.apache.brooklyn.api.framework.FrameworkLookup;
 import org.apache.brooklyn.api.mgmt.ManagementContext;
@@ -46,12 +56,14 @@
 import org.apache.brooklyn.core.BrooklynVersion;
 import org.apache.brooklyn.core.catalog.internal.CatalogBundleLoader;
 import org.apache.brooklyn.core.config.ConfigKeys;
-import org.apache.brooklyn.core.typereg.*;
-import org.apache.brooklyn.core.typereg.BrooklynCatalogBundleResolver.BundleInstallationOptions;
 import org.apache.brooklyn.core.mgmt.ha.OsgiBundleInstallationResult.ResultCode;
 import org.apache.brooklyn.core.server.BrooklynServerConfig;
 import org.apache.brooklyn.core.server.BrooklynServerPaths;
+import org.apache.brooklyn.core.typereg.BrooklynBomBundleCatalogBundleResolver;
+import org.apache.brooklyn.core.typereg.BrooklynCatalogBundleResolver.BundleInstallationOptions;
+import org.apache.brooklyn.core.typereg.BrooklynCatalogBundleResolvers;
 import org.apache.brooklyn.core.typereg.BundleUpgradeParser.CatalogUpgrades;
+import org.apache.brooklyn.core.typereg.RegisteredTypePredicates;
 import org.apache.brooklyn.util.collections.MutableList;
 import org.apache.brooklyn.util.collections.MutableMap;
 import org.apache.brooklyn.util.collections.MutableSet;
@@ -75,7 +87,6 @@
 import org.osgi.framework.Bundle;
 import org.osgi.framework.BundleException;
 import org.osgi.framework.Constants;
-import org.osgi.framework.FrameworkEvent;
 import org.osgi.framework.launch.Framework;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -96,6 +107,10 @@
 
     public static final ConfigKey<Boolean> OSGI_STARTUP_COMPLETE = ConfigKeys.newBooleanConfigKey("brooklyn.osgi.startup.complete");
 
+    public static final ConfigKey<String> PERSIST_MANAGED_BUNDLE_SYMBOLIC_NAME_EXCLUDE_REGEX = BrooklynServerConfig.PERSIST_MANAGED_BUNDLE_SYMBOLIC_NAME_EXCLUDE_REGEX;
+    public static final ConfigKey<String> PERSIST_MANAGED_BUNDLE_URL_EXCLUDE_REGEX = BrooklynServerConfig.PERSIST_MANAGED_BUNDLE_URL_EXCLUDE_REGEX;
+    public static final ConfigKey<String> PERSIST_MANAGED_BUNDLE_SYMBOLIC_NAME_INCLUDE_REGEX = BrooklynServerConfig.PERSIST_MANAGED_BUNDLE_SYMBOLIC_NAME_INCLUDE_REGEX;
+
     /* see `Osgis` class for info on starting framework etc */
     
     final ManagementContext mgmt;
@@ -870,4 +885,43 @@
         return managedBundlesRecord.fileFor(mb);
     }
 
+    private volatile Predicate<ManagedBundle> bundlePersistenceExclusionFilterCache;
+    public boolean isExcludedFromPersistence(ManagedBundle managedBundle) {
+        // We treat as "managed bundles" (to extract their catalog.bom) the contents of:
+        //   - org.apache.brooklyn.core
+        //   - org.apache.brooklyn.policy
+        //   - org.apache.brooklyn.test-framework
+        //   - org.apache.brooklyn.software-*
+        //   - org.apache.brooklyn.library-catalog
+        //   - org.apache.brooklyn.karaf-init (not sure why this one could end up in persisted state!)
+
+        // But we don't want to persist the entire brooklyn distro! Therefore default is to exclude those from persistence.
+        // Similarly for anything installed via mvn or classpath.
+
+        if (bundlePersistenceExclusionFilterCache == null) {
+            String regexSymnameInclude = mgmt.getConfig().getConfig(PERSIST_MANAGED_BUNDLE_SYMBOLIC_NAME_INCLUDE_REGEX);
+            String regexSymnameIncludeLegacy = mgmt.getConfig().getConfig(BrooklynServerConfig.PERSIST_MANAGED_BUNDLE_WHITELIST_REGEX);
+            String regexSymnameExclude = mgmt.getConfig().getConfig(PERSIST_MANAGED_BUNDLE_SYMBOLIC_NAME_EXCLUDE_REGEX);
+            String regexSymnameExcludeLegacy = mgmt.getConfig().getConfig(BrooklynServerConfig.PERSIST_MANAGED_BUNDLE_BLACKLIST_REGEX);
+            String regexUrlExclude = mgmt.getConfig().getConfig(PERSIST_MANAGED_BUNDLE_URL_EXCLUDE_REGEX);
+
+            final Pattern patternSymnameInclude = (regexSymnameInclude != null) ? Pattern.compile(regexSymnameInclude) : null;
+            final Pattern patternSymnameIncludeLegacy = (regexSymnameIncludeLegacy != null) ? Pattern.compile(regexSymnameIncludeLegacy) : null;
+            final Pattern patternUrlExclude = (regexUrlExclude != null) ? Pattern.compile(regexUrlExclude) : null;
+            final Pattern patternSymnameExclude = (regexSymnameExclude != null) ? Pattern.compile(regexSymnameExclude) : null;
+            final Pattern patternSymnameExcludeLegacy = (regexSymnameExcludeLegacy != null) ? Pattern.compile(regexSymnameExcludeLegacy) : null;
+
+            bundlePersistenceExclusionFilterCache = input -> {
+                String bundleName = input.getSymbolicName();
+                if (patternSymnameInclude != null && patternSymnameInclude.matcher(bundleName).matches()) return false;
+                if (patternSymnameIncludeLegacy != null && patternSymnameIncludeLegacy.matcher(bundleName).matches()) return false;
+                if (patternUrlExclude != null && input.getUrl()!=null && patternUrlExclude.matcher(input.getUrl()).matches()) return true;
+                if (patternSymnameExclude != null && patternSymnameExclude.matcher(bundleName).matches()) return true;
+                if (patternSymnameExcludeLegacy != null && patternSymnameExcludeLegacy.matcher(bundleName).matches()) return true;
+                return false;
+            };
+        }
+
+        return bundlePersistenceExclusionFilterCache.test(managedBundle);
+    }
 }
diff --git a/core/src/main/java/org/apache/brooklyn/core/mgmt/persist/BrooklynMementoPersisterToObjectStore.java b/core/src/main/java/org/apache/brooklyn/core/mgmt/persist/BrooklynMementoPersisterToObjectStore.java
index 82fd7c5..ed769ad 100644
--- a/core/src/main/java/org/apache/brooklyn/core/mgmt/persist/BrooklynMementoPersisterToObjectStore.java
+++ b/core/src/main/java/org/apache/brooklyn/core/mgmt/persist/BrooklynMementoPersisterToObjectStore.java
@@ -684,7 +684,7 @@
             futures.add(asyncUpdatePlaneId(newMemento.getPlaneId(), exceptionHandler));
             for (BrooklynObjectType type: BrooklynPersistenceUtils.STANDARD_BROOKLYN_OBJECT_TYPE_PERSISTENCE_ORDER) {
                 for (Map.Entry<String, String> entry : newMemento.getObjectsOfType(type).entrySet()) {
-                    addPersistContentIfManagedBundle(type, entry.getKey(), entry.getValue(), futures, exceptionHandler, contextDetails);
+                    addPersistContentIfManagedBundle(type, false, entry.getKey(), entry.getValue(), futures, exceptionHandler, contextDetails);
                     futures.add(asyncPersist(type.getSubPathName(), type, entry.getKey(), entry.getValue(), exceptionHandler));
                 }
             }
@@ -770,7 +770,7 @@
             for (BrooklynObjectType type: BrooklynPersistenceUtils.STANDARD_BROOKLYN_OBJECT_TYPE_PERSISTENCE_ORDER) {
                 for (Memento item : delta.getObjectsOfType(type)) {
                     if (!deletedIds.contains(item.getId())) {
-                        addPersistContentIfManagedBundle(type, item.getId(), ""+item.getCatalogItemId()+"/"+item.getDisplayName(), futures, exceptionHandler, null);
+                        addPersistContentIfManagedBundle(type, true, item.getId(), ""+item.getCatalogItemId()+"/"+item.getDisplayName(), futures, exceptionHandler, null);
                         futures.add(asyncPersist(type.getSubPathName(), item, exceptionHandler));
                     }
                 }
@@ -800,7 +800,7 @@
         return lastErrors;
     }
 
-    private void addPersistContentIfManagedBundle(final BrooklynObjectType type, final String id, final String summaryOrContents, List<ListenableFuture<?>> futures, final PersistenceExceptionHandler exceptionHandler, final @Nullable RebindManager deltaContext) {
+    private void addPersistContentIfManagedBundle(final BrooklynObjectType type, final boolean isDelta, final String id, final String summaryOrContents, List<ListenableFuture<?>> futures, final PersistenceExceptionHandler exceptionHandler, final @Nullable RebindManager deltaContext) {
         if (type==BrooklynObjectType.MANAGED_BUNDLE) {
             if (mgmt==null) {
                 throw new IllegalStateException("Cannot persist bundles without a management context");
@@ -818,16 +818,18 @@
             }
             
             if (mb instanceof BasicManagedBundle) {
-                if (((BasicManagedBundle)mb).getPersistenceNeeded()) {
+                if (!isDelta || ((BasicManagedBundle)mb).getPersistenceNeeded()) {
                     futures.add( executor.submit(new Runnable() {
                         @Override
                         public void run() {
-                            if (!((BasicManagedBundle)mb).getPersistenceNeeded()) {
+                            if (isDelta && !((BasicManagedBundle)mb).getPersistenceNeeded()) {
                                 // someone else persisted this (race)
                                 return;
                             }
-                            persist(type.getSubPathName(), type, id+".jar", com.google.common.io.Files.asByteSource(
-                                ((ManagementContextInternal)mgmt).getOsgiManager().get().getBundleFile(mb)), exceptionHandler);
+                            if (!isBundleOmittedFromPersistence(mb)) {
+                                persist(type.getSubPathName(), type, id + ".jar", com.google.common.io.Files.asByteSource(
+                                        ((ManagementContextInternal) mgmt).getOsgiManager().get().getBundleFile(mb)), exceptionHandler);
+                            }
                             ((BasicManagedBundle)mb).setPersistenceNeeded(false);
                         } }) );
                 }
@@ -835,6 +837,10 @@
         }
     }
 
+    private boolean isBundleOmittedFromPersistence(ManagedBundle mb) {
+        return ((ManagementContextInternal)mgmt).getOsgiManager().get().isExcludedFromPersistence(mb);
+    }
+
     @Override
     public void waitForWritesCompleted(Duration timeout) throws InterruptedException, TimeoutException {
         boolean locked = lock.readLock().tryLock(timeout.toMillisecondsRoundingUp(), TimeUnit.MILLISECONDS);
diff --git a/core/src/main/java/org/apache/brooklyn/core/server/BrooklynServerConfig.java b/core/src/main/java/org/apache/brooklyn/core/server/BrooklynServerConfig.java
index b1d7331..7ecfdc3 100644
--- a/core/src/main/java/org/apache/brooklyn/core/server/BrooklynServerConfig.java
+++ b/core/src/main/java/org/apache/brooklyn/core/server/BrooklynServerConfig.java
@@ -18,13 +18,12 @@
  */
 package org.apache.brooklyn.core.server;
 
-import static org.apache.brooklyn.core.config.ConfigKeys.newBooleanConfigKey;
-import static org.apache.brooklyn.core.config.ConfigKeys.newStringConfigKey;
-
 import java.net.URI;
 import java.util.List;
 import java.util.Map;
 
+import com.google.common.collect.ImmutableList;
+import com.google.common.reflect.TypeToken;
 import org.apache.brooklyn.api.mgmt.ManagementContext;
 import org.apache.brooklyn.config.ConfigKey;
 import org.apache.brooklyn.config.StringConfigMap;
@@ -36,8 +35,8 @@
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-import com.google.common.collect.ImmutableList;
-import com.google.common.reflect.TypeToken;
+import static org.apache.brooklyn.core.config.ConfigKeys.newBooleanConfigKey;
+import static org.apache.brooklyn.core.config.ConfigKeys.newStringConfigKey;
 
 /** Config keys for the brooklyn server */
 public class BrooklynServerConfig {
@@ -97,7 +96,7 @@
             "Whether a backup of in-memory state should be made to the backup persistence location on node demotion, "
             + "in case other nodes might write conflicting state", true);
 
-    /** @deprecated since 0.7.0, use {@link #PERSISTENCE_BACKUPS_ON_PROMOTION} and {@link #PERSISTENCE_BACKUPS_ON_DEMOTION},
+    /** @deprecated since 0.7.0, use {@link #PERSISTENCE_BACKUPS_REQUIRED_ON_PROMOTION} and {@link #PERSISTENCE_BACKUPS_REQUIRED_ON_DEMOTION},
      * which allow using a different target location and are supported on more environments (and now default to true) */
     @Deprecated
     public static final ConfigKey<Boolean> PERSISTENCE_BACKUPS_REQUIRED =
@@ -138,18 +137,31 @@
     public static final ConfigKey<Boolean> OSGI_CACHE_CLEAN = ConfigKeys.newBooleanConfigKey("brooklyn.osgi.cache.clean",
         "Whether to delete the OSGi directory before and after use; if unset, it will delete if the node ID forms part of the cache dir path (which by default it does) to avoid file leaks");
 
+    public static final ConfigKey<String> PERSIST_MANAGED_BUNDLE_SYMBOLIC_NAME_INCLUDE_REGEX = ConfigKeys.newStringConfigKey(
+            "brooklyn.persistence.bundle.include.symbolicName.regex",
+            "Regex for bundle symbolic names explicitly allowed to be persisted, taking precedence over exclude list; " +
+                    "bundles are included by default unless excluded, so things only need to be listed here if they want to override an exclusion",
+            null);
+
+    public static final ConfigKey<String> PERSIST_MANAGED_BUNDLE_URL_EXCLUDE_REGEX = ConfigKeys.newStringConfigKey(
+            "brooklyn.persistence.bundle.exclude.url.regex",
+            "Regex for bundle URLs explicitly excluded from persistence, unless symbolic name is in explicit include list",
+            "(mvn|classpath):.*");
+
+    public static final ConfigKey<String> PERSIST_MANAGED_BUNDLE_SYMBOLIC_NAME_EXCLUDE_REGEX = ConfigKeys.newStringConfigKey(
+            "brooklyn.persistence.bundle.exclude.symbolicName.regex",
+            "Regex for bundle symbolic names explicitly excluded from persistence (but include list takes precedence); "
+                    + "if not explicitly excluded by this or the URL exclusion, managed bundles will by default be peristed",
+            "org\\.apache\\.brooklyn\\..*");
+
+    @Deprecated /** @deprecated in favour of {@link #PERSIST_MANAGED_BUNDLE_SYMBOLIC_NAME_INCLUDE_REGEX} */
     public static final ConfigKey<String> PERSIST_MANAGED_BUNDLE_WHITELIST_REGEX = ConfigKeys.newStringConfigKey(
             "brooklyn.persistence.bundle.whitelist",
-            "Regex for bundle symbolic names explicitly allowed to be persisted (taking precedence over blacklist); "
-                    + "managed bundles will by default be peristed if not blacklisted; "
-                    + "they do not need to be explicitly whitelisted.",
-            null);
-    
+            "Legacy name for "+PERSIST_MANAGED_BUNDLE_SYMBOLIC_NAME_INCLUDE_REGEX.getName());
+    @Deprecated /** @deprecated in favour of {@link #PERSIST_MANAGED_BUNDLE_SYMBOLIC_NAME_EXCLUDE_REGEX} */
     public static final ConfigKey<String> PERSIST_MANAGED_BUNDLE_BLACKLIST_REGEX = ConfigKeys.newStringConfigKey(
             "brooklyn.persistence.bundle.blacklist",
-            "Regex for bundle symbolic names explicitly excluded from persistence (but whitelist takes precedence); "
-                    + "if not explicitly blacklisted, managed bundles will by default be peristed",
-            "org\\.apache\\.brooklyn\\..*");
+            "Legacy name for "+PERSIST_MANAGED_BUNDLE_SYMBOLIC_NAME_EXCLUDE_REGEX.getName());
 
     /** @see BrooklynServerPaths#getMgmtBaseDir(ManagementContext) */
     public static String getMgmtBaseDir(ManagementContext mgmt) {
diff --git a/core/src/main/java/org/apache/brooklyn/core/typereg/BasicManagedBundle.java b/core/src/main/java/org/apache/brooklyn/core/typereg/BasicManagedBundle.java
index 42f7dbd..e053daa 100644
--- a/core/src/main/java/org/apache/brooklyn/core/typereg/BasicManagedBundle.java
+++ b/core/src/main/java/org/apache/brooklyn/core/typereg/BasicManagedBundle.java
@@ -47,6 +47,8 @@
     private String format;
     private String url;
     private Credentials credentials;
+
+    /** pretty much redundant as it is put in the delta if changed, and included even if not needed when full checkpoint requested */
     private transient boolean persistenceNeeded = false;
 
     /** Creates an empty one, with an ID, expecting other fields will be populated. */
@@ -282,7 +284,7 @@
     }
 
     public void setPersistenceNeeded(boolean val) {
-        persistenceNeeded |= val;
+        persistenceNeeded = val;
     }
     public boolean getPersistenceNeeded() {
         return persistenceNeeded;
diff --git a/core/src/test/java/org/apache/brooklyn/core/mgmt/ha/BrooklynBomOsgiArchiveInstallerTest.java b/core/src/test/java/org/apache/brooklyn/core/mgmt/ha/BrooklynBomOsgiArchiveInstallerTest.java
index 8bd142e..b37457e 100644
--- a/core/src/test/java/org/apache/brooklyn/core/mgmt/ha/BrooklynBomOsgiArchiveInstallerTest.java
+++ b/core/src/test/java/org/apache/brooklyn/core/mgmt/ha/BrooklynBomOsgiArchiveInstallerTest.java
@@ -38,33 +38,33 @@
     // BrooklynMgmtUnitTestSupport, which does not expose `useOsgi` or `osgiReuse`
     
     @Test
-    public void testBlacklistPersistingOrgApacheBrooklyn() throws Exception {
-        OsgiManager osgiManager = newMockOsgiManager(mgmt);
+    public void testBundlePersistenceExclusionOrgApacheBrooklyn() throws Exception {
+        OsgiManager osgiManager = new OsgiManager(mgmt);
         BrooklynBomOsgiArchiveInstaller installer = new BrooklynBomOsgiArchiveInstaller(osgiManager, Mockito.mock(ManagedBundle.class), new ByteArrayInputStream(new byte[0]));
         
-        assertTrue(installer.isBlacklistedForPersistence(newMockManagedBundle("org.apache.brooklyn.core", "1.0.0")));
-        assertTrue(installer.isBlacklistedForPersistence(newMockManagedBundle("org.apache.brooklyn.mybundle", "1.0.0")));
-        assertFalse(installer.isBlacklistedForPersistence(newMockManagedBundle("org.apache.different", "1.0.0")));
+        assertTrue(installer.isExcludedFromPersistence(newMockManagedBundle("org.apache.brooklyn.core", "1.0.0")));
+        assertTrue(installer.isExcludedFromPersistence(newMockManagedBundle("org.apache.brooklyn.mybundle", "1.0.0")));
+        assertFalse(installer.isExcludedFromPersistence(newMockManagedBundle("org.apache.different", "1.0.0")));
     }
 
     @Test
-    public void testWhitelistPersistingBundle() throws Exception {
-        mgmt.getBrooklynProperties().put(BrooklynServerConfig.PERSIST_MANAGED_BUNDLE_WHITELIST_REGEX, "org\\.apache\\.brooklyn\\.mywhitelistedbundle");
-        OsgiManager osgiManager = newMockOsgiManager(mgmt);
+    public void testBundlePersistenceExclusionExplicitInclusion() throws Exception {
+        mgmt.getBrooklynProperties().put(BrooklynServerConfig.PERSIST_MANAGED_BUNDLE_SYMBOLIC_NAME_INCLUDE_REGEX, "org\\.apache\\.brooklyn\\.myincludebundle");
+        OsgiManager osgiManager = new OsgiManager(mgmt);
         BrooklynBomOsgiArchiveInstaller installer = new BrooklynBomOsgiArchiveInstaller(osgiManager, Mockito.mock(ManagedBundle.class), new ByteArrayInputStream(new byte[0]));
         
-        assertTrue(installer.isBlacklistedForPersistence(newMockManagedBundle("org.apache.brooklyn.core", "1.0.0")));
-        assertFalse(installer.isBlacklistedForPersistence(newMockManagedBundle("org.apache.brooklyn.mywhitelistedbundle", "1.0.0")));
+        assertTrue(installer.isExcludedFromPersistence(newMockManagedBundle("org.apache.brooklyn.core", "1.0.0")));
+        assertFalse(installer.isExcludedFromPersistence(newMockManagedBundle("org.apache.brooklyn.myincludebundle", "1.0.0")));
     }
 
     @Test
-    public void testCustomBlacklistPersistingBundle() throws Exception {
-        mgmt.getBrooklynProperties().put(BrooklynServerConfig.PERSIST_MANAGED_BUNDLE_BLACKLIST_REGEX, "org\\.example\\.myblacklistprefix.*");
-        OsgiManager osgiManager = newMockOsgiManager(mgmt);
+    public void testBundlePersistenceExclusionCustom() throws Exception {
+        mgmt.getBrooklynProperties().put(BrooklynServerConfig.PERSIST_MANAGED_BUNDLE_SYMBOLIC_NAME_EXCLUDE_REGEX, "org\\.example\\.myexcludeprefix.*");
+        OsgiManager osgiManager = new OsgiManager(mgmt);
         BrooklynBomOsgiArchiveInstaller installer = new BrooklynBomOsgiArchiveInstaller(osgiManager, Mockito.mock(ManagedBundle.class), new ByteArrayInputStream(new byte[0]));
         
-        assertTrue(installer.isBlacklistedForPersistence(newMockManagedBundle("org.example.myblacklistprefix.mysuffix", "1.0.0")));
-        assertFalse(installer.isBlacklistedForPersistence(newMockManagedBundle("org.apache.brooklyn.core", "1.0.0")));
+        assertTrue(installer.isExcludedFromPersistence(newMockManagedBundle("org.example.myexcludeprefix.mysuffix", "1.0.0")));
+        assertFalse(installer.isExcludedFromPersistence(newMockManagedBundle("org.apache.brooklyn.core", "1.0.0")));
     }
 
     @Test