diff --git a/liquibase-core/pom.xml b/liquibase-core/pom.xml index 70f2fbc5f60..0fdcaadc938 100644 --- a/liquibase-core/pom.xml +++ b/liquibase-core/pom.xml @@ -57,13 +57,13 @@ 1.3 test - + org.osgi org.osgi.core provided - 4.3.1 - + 5.0.0 + org.springframework @@ -155,10 +155,11 @@ org.apache.felix maven-bundle-plugin - 3.3.0 + 5.1.2 org.liquibase.core + liquibase.osgi.Activator javax.activation*;resolution:=optional, javax.servlet.*;version="[2.6,4)";resolution:=optional, @@ -167,6 +168,66 @@ org.yaml.snakeyaml.*, *;resolution:=optional + + osgi.serviceloader; osgi.serviceloader=liquibase.serializer.ChangeLogSerializer, + osgi.serviceloader; osgi.serviceloader=liquibase.parser.NamespaceDetails, + osgi.serviceloader; osgi.serviceloader=liquibase.database.Database, + osgi.serviceloader; osgi.serviceloader=liquibase.change.Change, + osgi.serviceloader; osgi.serviceloader=liquibase.database.DatabaseConnection, + osgi.serviceloader; osgi.serviceloader=liquibase.precondition.Precondition, + osgi.serviceloader; osgi.serviceloader=liquibase.serializer.SnapshotSerializer, + osgi.serviceloader; osgi.serviceloader=liquibase.configuration.AutoloadedConfigurations, + osgi.serviceloader; osgi.serviceloader=liquibase.diff.DiffGenerator, + osgi.serviceloader; osgi.serviceloader=liquibase.lockservice.LockService, + osgi.serviceloader; osgi.serviceloader=liquibase.changelog.ChangeLogHistoryService, + osgi.serviceloader; osgi.serviceloader=liquibase.datatype.LiquibaseDataType, + osgi.serviceloader; osgi.serviceloader=liquibase.configuration.ConfigurationValueProvider, + osgi.serviceloader; osgi.serviceloader=liquibase.logging.LogService, + osgi.serviceloader; osgi.serviceloader=liquibase.snapshot.SnapshotGenerator, + osgi.serviceloader; osgi.serviceloader=liquibase.parser.ChangeLogParser, + osgi.serviceloader; osgi.serviceloader=liquibase.servicelocator.ServiceLocator, + osgi.serviceloader; osgi.serviceloader=liquibase.diff.compare.DatabaseObjectComparator, + osgi.serviceloader; osgi.serviceloader=liquibase.command.LiquibaseCommand, + osgi.serviceloader; osgi.serviceloader=liquibase.license.LicenseService, + osgi.serviceloader; osgi.serviceloader=liquibase.diff.output.changelog.ChangeGenerator, + osgi.serviceloader; osgi.serviceloader=liquibase.executor.Executor, + osgi.serviceloader; osgi.serviceloader=liquibase.structure.DatabaseObject, + osgi.serviceloader; osgi.serviceloader=liquibase.parser.SnapshotParser, + osgi.serviceloader; osgi.serviceloader=liquibase.hub.HubService, + osgi.serviceloader; osgi.serviceloader=liquibase.command.CommandStep, + osgi.serviceloader; osgi.serviceloader=liquibase.sqlgenerator.SqlGenerator + + + osgi.extender; filter:="(osgi.extender=osgi.serviceloader.registrar)", + osgi.extender; filter:="(osgi.extender=osgi.serviceloader.processor)", + osgi.serviceloader; filter:="(osgi.serviceloader=liquibase.serializer.ChangeLogSerializer)"; cardinality:=multiple, + osgi.serviceloader; filter:="(osgi.serviceloader=liquibase.parser.NamespaceDetails)"; cardinality:=multiple, + osgi.serviceloader; filter:="(osgi.serviceloader=liquibase.database.Database)"; cardinality:=multiple, + osgi.serviceloader; filter:="(osgi.serviceloader=liquibase.change.Change)"; cardinality:=multiple, + osgi.serviceloader; filter:="(osgi.serviceloader=liquibase.database.DatabaseConnection)"; cardinality:=multiple, + osgi.serviceloader; filter:="(osgi.serviceloader=liquibase.precondition.Precondition)"; cardinality:=multiple, + osgi.serviceloader; filter:="(osgi.serviceloader=liquibase.serializer.SnapshotSerializer)"; cardinality:=multiple, + osgi.serviceloader; filter:="(osgi.serviceloader=liquibase.configuration.AutoloadedConfigurations)"; cardinality:=multiple, + osgi.serviceloader; filter:="(osgi.serviceloader=liquibase.diff.DiffGenerator)"; cardinality:=multiple, + osgi.serviceloader; filter:="(osgi.serviceloader=liquibase.lockservice.LockService)"; cardinality:=multiple, + osgi.serviceloader; filter:="(osgi.serviceloader=liquibase.changelog.ChangeLogHistoryService)"; cardinality:=multiple, + osgi.serviceloader; filter:="(osgi.serviceloader=liquibase.datatype.LiquibaseDataType)"; cardinality:=multiple, + osgi.serviceloader; filter:="(osgi.serviceloader=liquibase.configuration.ConfigurationValueProvider)"; cardinality:=multiple, + osgi.serviceloader; filter:="(osgi.serviceloader=liquibase.logging.LogService)"; cardinality:=multiple, + osgi.serviceloader; filter:="(osgi.serviceloader=liquibase.snapshot.SnapshotGenerator)"; cardinality:=multiple, + osgi.serviceloader; filter:="(osgi.serviceloader=liquibase.parser.ChangeLogParser)"; cardinality:=multiple, + osgi.serviceloader; filter:="(osgi.serviceloader=liquibase.servicelocator.ServiceLocator)"; cardinality:=multiple, + osgi.serviceloader; filter:="(osgi.serviceloader=liquibase.diff.compare.DatabaseObjectComparator)"; cardinality:=multiple, + osgi.serviceloader; filter:="(osgi.serviceloader=liquibase.command.LiquibaseCommand)"; cardinality:=multiple, + osgi.serviceloader; filter:="(osgi.serviceloader=liquibase.license.LicenseService)"; cardinality:=multiple, + osgi.serviceloader; filter:="(osgi.serviceloader=liquibase.diff.output.changelog.ChangeGenerator)"; cardinality:=multiple, + osgi.serviceloader; filter:="(osgi.serviceloader=liquibase.executor.Executor)"; cardinality:=multiple, + osgi.serviceloader; filter:="(osgi.serviceloader=liquibase.structure.DatabaseObject)"; cardinality:=multiple, + osgi.serviceloader; filter:="(osgi.serviceloader=liquibase.parser.SnapshotParser)"; cardinality:=multiple, + osgi.serviceloader; filter:="(osgi.serviceloader=liquibase.hub.HubService)"; cardinality:=multiple, + osgi.serviceloader; filter:="(osgi.serviceloader=liquibase.command.CommandStep)"; cardinality:=multiple, + osgi.serviceloader; filter:="(osgi.serviceloader=liquibase.sqlgenerator.SqlGenerator)"; cardinality:=multiple + diff --git a/liquibase-core/src/main/java/liquibase/Scope.java b/liquibase-core/src/main/java/liquibase/Scope.java index 25a5aa3adcb..0910d57134c 100644 --- a/liquibase-core/src/main/java/liquibase/Scope.java +++ b/liquibase-core/src/main/java/liquibase/Scope.java @@ -11,6 +11,7 @@ import liquibase.logging.Logger; import liquibase.logging.core.JavaLogService; import liquibase.logging.core.LogServiceFactory; +import liquibase.osgi.Activator; import liquibase.resource.ClassLoaderResourceAccessor; import liquibase.resource.ResourceAccessor; import liquibase.servicelocator.ServiceLocator; @@ -55,6 +56,7 @@ public enum Attr { fileEncoding, databaseChangeLog, changeSet, + osgiPlatform } private static ScopeManager scopeManager; @@ -95,6 +97,7 @@ public static Scope getCurrentScope() { } rootScope.values.put(Attr.serviceLocator.name(), serviceLocator); + rootScope.values.put(Attr.osgiPlatform.name(), Activator.OSGIContainerChecker.isOsgiPlatform()); } return scopeManager.getCurrentScope(); } diff --git a/liquibase-core/src/main/java/liquibase/change/custom/CustomChangeWrapper.java b/liquibase-core/src/main/java/liquibase/change/custom/CustomChangeWrapper.java index 1f4e4f94d9a..f86c0b68025 100644 --- a/liquibase-core/src/main/java/liquibase/change/custom/CustomChangeWrapper.java +++ b/liquibase-core/src/main/java/liquibase/change/custom/CustomChangeWrapper.java @@ -14,6 +14,7 @@ import liquibase.util.ObjectUtil; import java.util.*; +import liquibase.util.OsgiUtil; /** * Adapts CustomChange implementations to the standard change system used by Liquibase. @@ -69,16 +70,21 @@ public CustomChangeWrapper setClass(String className) throws CustomChangeExcepti return this; } this.className = className; - try { + try { + Boolean osgiPlatform = Scope.getCurrentScope().get(Scope.Attr.osgiPlatform, Boolean.class); + if (Boolean.TRUE.equals(osgiPlatform)) { + customChange = (CustomChange)OsgiUtil.loadClass(className).getConstructor().newInstance(); + } else { + try { + customChange = (CustomChange) Class.forName(className, true, Scope.getCurrentScope().getClassLoader()).getConstructor().newInstance(); + } catch (ClassCastException e) { //fails in Ant in particular try { - customChange = (CustomChange) Class.forName(className, true, Scope.getCurrentScope().getClassLoader()).getConstructor().newInstance(); - } catch (ClassCastException e) { //fails in Ant in particular - try { - customChange = (CustomChange) Thread.currentThread().getContextClassLoader().loadClass(className).getConstructor().newInstance(); - } catch (ClassNotFoundException e1) { - customChange = (CustomChange) Class.forName(className).getConstructor().newInstance(); - } + customChange = (CustomChange) Thread.currentThread().getContextClassLoader().loadClass(className).getConstructor().newInstance(); + } catch (ClassNotFoundException e1) { + customChange = (CustomChange) Class.forName(className).getConstructor().newInstance(); } + } + } } catch (Exception e) { throw new CustomChangeException(e); } @@ -316,7 +322,12 @@ public void customLoadLogic(ParsedNode parsedNode, ResourceAccessor resourceAcce CustomChange customChange = null; try { - customChange = (CustomChange) Class.forName(className, false, Scope.getCurrentScope().getClassLoader()).getConstructor().newInstance(); + Boolean osgiPlatform = Scope.getCurrentScope().get(Scope.Attr.osgiPlatform, Boolean.class); + if (Boolean.TRUE.equals(osgiPlatform)) { + customChange = (CustomChange)OsgiUtil.loadClass(className).getConstructor().newInstance(); + } else { + customChange = (CustomChange) Class.forName(className, false, Scope.getCurrentScope().getClassLoader()).getConstructor().newInstance(); + } } catch (Exception e) { throw new UnexpectedLiquibaseException(e); } diff --git a/liquibase-core/src/main/java/liquibase/osgi/Activator.java b/liquibase-core/src/main/java/liquibase/osgi/Activator.java new file mode 100644 index 00000000000..673a53cebe1 --- /dev/null +++ b/liquibase-core/src/main/java/liquibase/osgi/Activator.java @@ -0,0 +1,108 @@ +package liquibase.osgi; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; +import liquibase.osgi.Activator.LiquibaseBundle; +import org.osgi.framework.Bundle; +import org.osgi.framework.BundleActivator; +import org.osgi.framework.BundleContext; +import org.osgi.framework.BundleEvent; +import org.osgi.util.tracker.BundleTracker; +import org.osgi.util.tracker.BundleTrackerCustomizer; + +public class Activator implements BundleActivator, BundleTrackerCustomizer { + + private static final String LIQUIBASE_CUSTOM_CHANGE_WRAPPER_PACKAGES = "Liquibase-Custom-Change-Packages"; + private BundleTracker bundleTracker; + private static final List liquibaseBundles = new CopyOnWriteArrayList<>(); + + @Override + public void start(final BundleContext bc) throws Exception { + OSGIContainerChecker.osgiPlatform(); + bundleTracker = new BundleTracker<>(bc, Bundle.ACTIVE, this); + bundleTracker.open(); + } + + @Override + public void stop(BundleContext context) throws Exception { + bundleTracker.close(); + liquibaseBundles.clear(); + } + + public static List getLiquibaseBundles() { + return Collections.unmodifiableList(liquibaseBundles); + } + + @Override + public LiquibaseBundle addingBundle(Bundle bundle, BundleEvent event) { + if (bundle.getBundleId() == 0) { + return null; + } + String customWrapperPackages = (String) bundle.getHeaders().get(LIQUIBASE_CUSTOM_CHANGE_WRAPPER_PACKAGES); + if (customWrapperPackages != null) { + LiquibaseBundle lb = new LiquibaseBundle(bundle, customWrapperPackages); + liquibaseBundles.add(lb); + return lb; + } + return null; + } + + @Override + public void modifiedBundle(Bundle bundle, BundleEvent event, LiquibaseBundle liquibaseBundle) { + // nothing to do + } + + @Override + public void removedBundle(Bundle bundle, BundleEvent event, LiquibaseBundle liquibaseBundle) { + if (liquibaseBundle != null) { + liquibaseBundles.remove(liquibaseBundle); + } + } + + public static class LiquibaseBundle { + + private final Bundle bundle; + private final List allowedPackages; + + public LiquibaseBundle(Bundle bundle, String allowedPackages) { + if (bundle == null) { + throw new IllegalArgumentException("bundle cannot be empty"); + } + if (allowedPackages == null || allowedPackages.isEmpty()) { + throw new IllegalArgumentException("packages cannot be empty"); + } + this.bundle = bundle; + this.allowedPackages = Collections.unmodifiableList(Arrays.asList(allowedPackages.split(","))); + } + + public Bundle getBundle() { + return bundle; + } + + public boolean allowedAllPackages() { + return allowedPackages.size() == 1 + && "*".equals(allowedPackages.get(0)); + } + + public List getAllowedPackages() { + return allowedPackages; + } + + } + + public static class OSGIContainerChecker { + + private static volatile boolean osgiPlatform = false; + + public static boolean isOsgiPlatform() { + return osgiPlatform; + } + + static void osgiPlatform() { + osgiPlatform = true; + } + } + +} diff --git a/liquibase-core/src/main/java/liquibase/util/LiquibaseUtil.java b/liquibase-core/src/main/java/liquibase/util/LiquibaseUtil.java index 8b56d2ca485..de89b1868c8 100644 --- a/liquibase-core/src/main/java/liquibase/util/LiquibaseUtil.java +++ b/liquibase-core/src/main/java/liquibase/util/LiquibaseUtil.java @@ -1,6 +1,8 @@ package liquibase.util; import liquibase.Scope; +import org.osgi.framework.Bundle; +import org.osgi.framework.FrameworkUtil; import java.io.IOException; import java.io.InputStream; @@ -54,19 +56,37 @@ public static String getBuildNumber() { // the jar file. private static String getBuildInfo(String propertyId) { if (liquibaseBuildProperties == null) { - try { - liquibaseBuildProperties = new Properties(); - final Enumeration propertiesUrls = Scope.getCurrentScope().getClassLoader().getResources("liquibase.build.properties"); - while (propertiesUrls.hasMoreElements()) { - final URL url = propertiesUrls.nextElement(); - try (InputStream buildProperties = url.openStream()) { + Boolean osgiPlatform = Scope.getCurrentScope().get(Scope.Attr.osgiPlatform, Boolean.class); + if (Boolean.TRUE.equals(osgiPlatform)) { + Bundle bundle = FrameworkUtil.getBundle(LiquibaseUtil.class); + URL propURL = bundle.getEntry("liquibase.build.properties"); + if (propURL == null) { + Scope.getCurrentScope().getLog(LiquibaseUtil.class).severe("Cannot read liquibase.build.properties"); + } else { + try (InputStream buildProperties = propURL.openStream()) { + liquibaseBuildProperties = new Properties(); if (buildProperties != null) { liquibaseBuildProperties.load(buildProperties); } + } catch (IOException e) { + Scope.getCurrentScope().getLog(LiquibaseUtil.class).severe("Cannot read liquibase.build.properties", e); } } - } catch (IOException e) { - Scope.getCurrentScope().getLog(LiquibaseUtil.class).severe("Cannot read liquibase.build.properties", e); + } else { + try { + liquibaseBuildProperties = new Properties(); + final Enumeration propertiesUrls = Scope.getCurrentScope().getClassLoader().getResources("liquibase.build.properties"); + while (propertiesUrls.hasMoreElements()) { + final URL url = propertiesUrls.nextElement(); + try (InputStream buildProperties = url.openStream()) { + if (buildProperties != null) { + liquibaseBuildProperties.load(buildProperties); + } + } + } + } catch (IOException e) { + Scope.getCurrentScope().getLog(LiquibaseUtil.class).severe("Cannot read liquibase.build.properties", e); + } } } diff --git a/liquibase-core/src/main/java/liquibase/util/OsgiUtil.java b/liquibase-core/src/main/java/liquibase/util/OsgiUtil.java new file mode 100644 index 00000000000..71a39077f77 --- /dev/null +++ b/liquibase-core/src/main/java/liquibase/util/OsgiUtil.java @@ -0,0 +1,65 @@ +package liquibase.util; + +import java.util.List; +import java.util.stream.Collectors; +import liquibase.osgi.Activator; +import liquibase.osgi.Activator.LiquibaseBundle; + +public final class OsgiUtil { + + private OsgiUtil() { + } + + /** + * try to load a class under OSGI environment. It will try to load the class + * from all liquibase bundles registered via + * {@link Activator Activator} + * + * @param + * @param className name of class + * @return + * @throws ClassNotFoundException + */ + public static Class loadClass(String className) throws ClassNotFoundException { + List liquibaseBundles = Activator.getLiquibaseBundles(); + for (LiquibaseBundle lb : liquibaseBundles) { + try { + Class clazz = (Class) lb.getBundle().loadClass(className); + if (!isClassAllowed(lb, clazz)) { + throw new ClassNotFoundException("Class is not allowed to load, class:" + className + " bundles:" + + liquibaseBundles.stream().map(i -> i.getBundle().getSymbolicName()) + .collect(Collectors.joining(","))); + } + return clazz; + } catch (ClassNotFoundException ex) { + // nothing to do + } + } + throw new ClassNotFoundException("Cannot find class:" + className + " bundles:" + + liquibaseBundles.stream().map(i -> i.getBundle().getSymbolicName()) + .collect(Collectors.joining(","))); + } + + /** + * + * @param clazz + * @return true is a class is allowed + * @throws java.lang.ClassNotFoundException + */ + private static boolean isClassAllowed(LiquibaseBundle liquibaseBundle, Class clazz) { + if (liquibaseBundle.allowedAllPackages()) { + return true; + } + for (String allowedPackage : liquibaseBundle.getAllowedPackages()) { + Package pkg = clazz.getPackage(); + if (pkg != null) { + String pkgName = pkg.getName(); + if (allowedPackage.equals(pkgName) || allowedPackage.startsWith(pkgName + ".")) { + return true; + } + } + } + return false; + } + +}