diff --git a/core/src/main/java/lucee/runtime/config/ConfigAdmin.java b/core/src/main/java/lucee/runtime/config/ConfigAdmin.java index 00495c56b8a..44302021bfb 100755 --- a/core/src/main/java/lucee/runtime/config/ConfigAdmin.java +++ b/core/src/main/java/lucee/runtime/config/ConfigAdmin.java @@ -5064,14 +5064,23 @@ public void updateRHExtension(Config config, RHExtension rhext, boolean reload, if (!ArrayUtil.isEmpty(rhext.getMavens())) { Iterator> itl = rhext.getMavens().iterator(); GAVSO gavso; + boolean mavenUpdated = false; while (itl.hasNext()) { gavso = MavenUtil.toGAVSO(itl.next()); if (gavso != null) { _updateMaven(gavso); reloadNecessary = true; + mavenUpdated = true; } logger.info("extension", "Update maven endpoint [" + gavso + "] from extension [" + rhext.getName() + ":" + rhext.getVersion() + "]"); } + // LDEV-6297: refresh cached JavaSettings so the new maven entries reach the RPC classloader without a server restart + if (mavenUpdated && config instanceof ConfigImpl) { + ConfigWebFactory._loadJavaSettings(null, (ConfigImpl) config, root, logger); + // also invalidate the JVM-level default classloader cached in ModernApplicationContext, + // otherwise CFML createObject calls keep getting the stale classloader from before the refresh + lucee.runtime.listener.ModernApplicationContext.resetDefaultClassLoader(); + } } // update Resource diff --git a/core/src/main/java/lucee/runtime/config/ConfigWebFactory.java b/core/src/main/java/lucee/runtime/config/ConfigWebFactory.java index 402536473f4..82ec0e8ac76 100644 --- a/core/src/main/java/lucee/runtime/config/ConfigWebFactory.java +++ b/core/src/main/java/lucee/runtime/config/ConfigWebFactory.java @@ -4388,7 +4388,7 @@ else if (hasCS) { } } - private static void _loadJavaSettings(ConfigServerImpl configServer, ConfigImpl config, Struct root, Log log) { + static void _loadJavaSettings(ConfigServerImpl configServer, ConfigImpl config, Struct root, Log log) { try { if (config instanceof ConfigServerImpl) { diff --git a/core/src/main/java/lucee/runtime/extension/RHExtension.java b/core/src/main/java/lucee/runtime/extension/RHExtension.java index 20134e46291..80b1224265f 100644 --- a/core/src/main/java/lucee/runtime/extension/RHExtension.java +++ b/core/src/main/java/lucee/runtime/extension/RHExtension.java @@ -83,6 +83,7 @@ import lucee.runtime.functions.conversion.DeserializeJSON; import lucee.runtime.interpreter.JSONExpressionInterpreter; import lucee.runtime.listener.SerializationSettings; +import lucee.runtime.mvn.MavenUtil; import lucee.runtime.op.Caster; import lucee.runtime.op.Decision; import lucee.runtime.osgi.BundleFile; @@ -723,7 +724,7 @@ private void readSearch(String label, String str, Log logger) { private void readMaven(String label, String str, Log logger) { if (!StringUtil.isEmpty(str, true)) { - mavens = toSettings(logger, str); + mavens = toMavenSettings(logger, str); mavensJson = str; } if (mavens == null) mavens = new ArrayList>(); @@ -1337,6 +1338,51 @@ private static void _toSettings(List list, Log log, String str, boolean valueAsS return; } + // LDEV-6297: accepts JSON or gradle GAV-comma; JSON tried first to preserve in-the-wild 6.2 manifests + private static List> toMavenSettings(Log log, String str) { + List> list = new ArrayList<>(); + + boolean parsedJson = false; + try { + Object res = DeserializeJSON.call(null, str); + if (Decision.isStruct(res)) { + parsedJson = true; + _toSetting(list, Caster.toMap(res), true); + } + else if (Decision.isArray(res)) { + parsedJson = true; + Iterator it = Caster.toList(res).iterator(); + while (it.hasNext()) { + _toSetting(list, Caster.toMap(it.next()), true); + } + } + } + catch (Throwable t) { + ExceptionUtil.rethrowIfNecessary(t); + // not JSON — fall through to gradle GAV-comma + } + if (parsedJson) return list; + + List gavsos = MavenUtil.toGAVSOs(str, null); + if (!gavsos.isEmpty()) { + for (MavenUtil.GAVSO gavso: gavsos) { + Map m = new HashMap<>(); + m.put("groupId", gavso.g); + m.put("artifactId", gavso.a); + if (gavso.v != null) m.put("version", gavso.v); + if (gavso.s != null) m.put("scope", gavso.s); + if (gavso.o != null) m.put("optional", gavso.o); + if (gavso.c != null) m.put("checksum", gavso.c); + list.add(m); + } + return list; + } + + log.error("Extension Installation", + "Could not parse maven manifest field — expected JSON or 'group:artifact:version' form: " + str); + return list; + } + private static void _toSetting(List list, Map src, boolean valueAsString) throws PageException { Entry e; Iterator it = src.entrySet().iterator(); diff --git a/core/src/main/java/lucee/runtime/listener/ModernApplicationContext.java b/core/src/main/java/lucee/runtime/listener/ModernApplicationContext.java index c597e61ce64..22e819a87ee 100644 --- a/core/src/main/java/lucee/runtime/listener/ModernApplicationContext.java +++ b/core/src/main/java/lucee/runtime/listener/ModernApplicationContext.java @@ -1794,6 +1794,13 @@ public static ClassLoader getDefaultClassLoader(ConfigWeb config) throws IOExcep return defaultClassLoader; } + // LDEV-6297: invalidate the cached default classloader so the next request rebuilds it from the (possibly just-refreshed) ConfigServer JavaSettings + public static void resetDefaultClassLoader() { + synchronized (token) { + defaultClassLoader = null; + } + } + @Override public Map getTagAttributeDefaultValues(PageContext pc, String tagClassName) { if (!initDefaultAttributeValues) { diff --git a/test/tickets/LDEV6297.cfc b/test/tickets/LDEV6297.cfc new file mode 100644 index 00000000000..e05c4ae8495 --- /dev/null +++ b/test/tickets/LDEV6297.cfc @@ -0,0 +1,98 @@ +component extends="org.lucee.cfml.test.LuceeTestCase" { + + function run( testResults, testBox ) { + describe( "LDEV-6297: extension manifest 'maven:' field accepts JSON and gradle GAV-comma forms", function() { + + it( title="single GAV gradle string", body=function() { + var result = parseManifest( "org.postgresql:postgresql:42.7.1" ); + expect( result.size() ).toBe( 1 ); + expect( result.get( 0 ).get( "groupId" ) ).toBe( "org.postgresql" ); + expect( result.get( 0 ).get( "artifactId" ) ).toBe( "postgresql" ); + expect( result.get( 0 ).get( "version" ) ).toBe( "42.7.1" ); + }); + + it( title="multiple GAVs comma-separated", body=function() { + var result = parseManifest( "org.postgresql:postgresql:42.7.1,org.apache.poi:poi:5.5.1" ); + expect( result.size() ).toBe( 2 ); + expect( result.get( 0 ).get( "groupId" ) ).toBe( "org.postgresql" ); + expect( result.get( 0 ).get( "version" ) ).toBe( "42.7.1" ); + expect( result.get( 1 ).get( "groupId" ) ).toBe( "org.apache.poi" ); + expect( result.get( 1 ).get( "artifactId" ) ).toBe( "poi" ); + expect( result.get( 1 ).get( "version" ) ).toBe( "5.5.1" ); + }); + + it( title="JSON struct (single entry)", body=function() { + var result = parseManifest( "{'groupId':'org.postgresql','artifactId':'postgresql','version':'42.7.1'}" ); + expect( result.size() ).toBe( 1 ); + expect( result.get( 0 ).get( "groupId" ) ).toBe( "org.postgresql" ); + expect( result.get( 0 ).get( "artifactId" ) ).toBe( "postgresql" ); + expect( result.get( 0 ).get( "version" ) ).toBe( "42.7.1" ); + }); + + it( title="JSON array of structs", body=function() { + var result = parseManifest( "[{'groupId':'org.postgresql','artifactId':'postgresql','version':'42.7.1'},{'groupId':'org.apache.poi','artifactId':'poi','version':'5.5.1'}]" ); + expect( result.size() ).toBe( 2 ); + expect( result.get( 0 ).get( "groupId" ) ).toBe( "org.postgresql" ); + expect( result.get( 1 ).get( "groupId" ) ).toBe( "org.apache.poi" ); + expect( result.get( 1 ).get( "version" ) ).toBe( "5.5.1" ); + }); + + it( title="JSON array with short-key aliases (g/a/v)", body=function() { + // short keys are passed through as-is; downstream MavenUtil.toGAVSO( map ) reads both forms + var result = parseManifest( "[{'g':'org.postgresql','a':'postgresql','v':'42.7.1'}]" ); + expect( result.size() ).toBe( 1 ); + expect( result.get( 0 ).get( "g" ) ).toBe( "org.postgresql" ); + expect( result.get( 0 ).get( "a" ) ).toBe( "postgresql" ); + expect( result.get( 0 ).get( "v" ) ).toBe( "42.7.1" ); + }); + + it( title="whitespace-padded GAV-comma form", body=function() { + var result = parseManifest( " org.postgresql:postgresql:42.7.1 , org.apache.poi:poi:5.5.1 " ); + expect( result.size() ).toBe( 2 ); + expect( result.get( 0 ).get( "groupId" ) ).toBe( "org.postgresql" ); + expect( result.get( 0 ).get( "version" ) ).toBe( "42.7.1" ); + expect( result.get( 1 ).get( "artifactId" ) ).toBe( "poi" ); + }); + + it( title="GAV with scope token", body=function() { + var result = parseManifest( "org.postgresql:postgresql:42.7.1:runtime" ); + expect( result.size() ).toBe( 1 ); + expect( result.get( 0 ).get( "groupId" ) ).toBe( "org.postgresql" ); + expect( result.get( 0 ).get( "version" ) ).toBe( "42.7.1" ); + expect( result.get( 0 ).get( "scope" ) ).toBe( "runtime" ); + }); + + it( title="garbage value parses as neither, returns empty list", body=function() { + // a single 'Could not parse maven manifest field' error is logged; mavens stays empty + var result = parseManifest( "this is not a parseable manifest value" ); + expect( result.size() ).toBe( 0 ); + }); + + }); + } + + private any function parseManifest( required string str ) { + var classCls = createObject( "java", "java.lang.Class" ); + var rhExtClass = classCls.forName( "lucee.runtime.extension.RHExtension" ); + var logClass = classCls.forName( "lucee.commons.io.log.Log" ); + var stringClass = classCls.forName( "java.lang.String" ); + + var method = rhExtClass.getDeclaredMethod( "toMavenSettings", classArray( [ logClass, stringClass ] ) ); + method.setAccessible( true ); + + var log = getPageContext().getConfig().getLog( "application" ); + return method.invoke( nullValue(), [ log, arguments.str ] ); + } + + // build a real java.lang.Class[] array for getDeclaredMethod's param-types argument + private any function classArray( required array classes ) { + var classCls = createObject( "java", "java.lang.Class" ); + var arrayType = createObject( "java", "java.lang.reflect.Array" ); + var arr = arrayType.newInstance( classCls.getClass(), arrayLen( arguments.classes ) ); + for ( var i = 1; i <= arrayLen( arguments.classes ); i++ ) { + arrayType.set( arr, i - 1, arguments.classes[ i ] ); + } + return arr; + } + +}