diff --git a/gradle.properties b/gradle.properties index cb06ea2e9..1cf9f87a5 100644 --- a/gradle.properties +++ b/gradle.properties @@ -10,5 +10,5 @@ mavenPassword=YourPassword # When updating the version, please as well consider: # - here-naksha-lib-core/src/main/com/here/naksha/lib/core/NakshaVersion (static property: latest) # - here-naksha-app-service/src/main/resources/swagger/openapi.yaml (info.version property) -version=2.2.1 +version=2.2.2 diff --git a/here-naksha-app-service/src/main/resources/swagger/openapi.yaml b/here-naksha-app-service/src/main/resources/swagger/openapi.yaml index bb3e48c6a..8a110d2fa 100644 --- a/here-naksha-app-service/src/main/resources/swagger/openapi.yaml +++ b/here-naksha-app-service/src/main/resources/swagger/openapi.yaml @@ -12,7 +12,7 @@ servers: info: title: "Naksha Hub-API" description: "Naksha Hub-API is a REST API to provide simple access to geo data." - version: "2.2.1" + version: "2.2.2" security: - AccessToken: [ ] diff --git a/here-naksha-lib-core/src/main/java/com/here/naksha/lib/core/IExtensionInit.java b/here-naksha-lib-core/src/main/java/com/here/naksha/lib/core/IExtensionInit.java new file mode 100644 index 000000000..33d21a305 --- /dev/null +++ b/here-naksha-lib-core/src/main/java/com/here/naksha/lib/core/IExtensionInit.java @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2017-2024 HERE Europe B.V. + * + * 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. + * + * SPDX-License-Identifier: Apache-2.0 + * License-Filename: LICENSE + */ +package com.here.naksha.lib.core; + +import com.here.naksha.lib.core.models.features.Extension; + +/** + * Naksha Extension Interface for all extensions providing initClassName. + */ +public interface IExtensionInit { + + /** + * Initializes the extension with the specified hub and extension parameters. + * This method should be called to set up any necessary resources or configurations + * required by the extension to operate correctly. + * @param hub The hub instance to be used by the extension. + * @param extension Extension configuration supplied as part of deployment pipeline for respective Extension and sub-env. + */ + void init(INaksha hub, Extension extension); + + /** + * Closes the extension. This method should be called to ensure proper + * cleanup when the extension is no longer needed. + */ + void close(); +} diff --git a/here-naksha-lib-core/src/main/java/com/here/naksha/lib/core/NakshaVersion.java b/here-naksha-lib-core/src/main/java/com/here/naksha/lib/core/NakshaVersion.java index 0cfa490c6..900ce433e 100644 --- a/here-naksha-lib-core/src/main/java/com/here/naksha/lib/core/NakshaVersion.java +++ b/here-naksha-lib-core/src/main/java/com/here/naksha/lib/core/NakshaVersion.java @@ -59,12 +59,13 @@ public class NakshaVersion implements Comparable { public static final String v2_1_1 = "2.1.1"; public static final String v2_2_0 = "2.2.0"; public static final String v2_2_1 = "2.2.1"; + public static final String v2_2_2 = "2.2.2"; /** * The latest version of the naksha-extension stored in the resources. */ @AvailableSince(v2_0_5) - public static final NakshaVersion latest = of(v2_2_1); + public static final NakshaVersion latest = of(v2_2_2); private final int major; private final int minor; diff --git a/here-naksha-lib-ext-manager/src/main/java/com/here/naksha/lib/extmanager/ExtensionCache.java b/here-naksha-lib-ext-manager/src/main/java/com/here/naksha/lib/extmanager/ExtensionCache.java index e45229fb1..77b1b2d66 100644 --- a/here-naksha-lib-ext-manager/src/main/java/com/here/naksha/lib/extmanager/ExtensionCache.java +++ b/here-naksha-lib-ext-manager/src/main/java/com/here/naksha/lib/extmanager/ExtensionCache.java @@ -18,6 +18,7 @@ */ package com.here.naksha.lib.extmanager; +import com.here.naksha.lib.core.IExtensionInit; import com.here.naksha.lib.core.INaksha; import com.here.naksha.lib.core.SimpleTask; import com.here.naksha.lib.core.models.ExtensionConfig; @@ -29,7 +30,6 @@ import com.here.naksha.lib.extmanager.models.KVPair; import java.io.File; import java.io.IOException; -import java.lang.reflect.InvocationTargetException; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -46,10 +46,10 @@ */ public class ExtensionCache { private static final @NotNull Logger logger = LoggerFactory.getLogger(ExtensionCache.class); - private static final ConcurrentHashMap> loaderCache = - new ConcurrentHashMap<>(); + private static final ConcurrentHashMap loaderCache = new ConcurrentHashMap<>(); private static final Map jarClientMap = new HashMap<>(); private final @NotNull INaksha naksha; + private static final String WHITE_LIST_CLASSES = "whitelistClasses"; static { jarClientMap.put(JarClientType.S3.getType(), new AmazonS3Helper()); @@ -90,10 +90,8 @@ protected void buildExtensionCache(ExtensionConfig extensionConfig) { for (String key : loaderCache.keySet()) { if (!extIds.contains(key)) { - loaderCache.remove(key); - PluginCache.removeExtensionCache(key); - logger.info("Extension {} removed from cache.", key); - } + removeExtensionFromCache(key); + } } logger.info("Extension cache size " + loaderCache.size()); } @@ -103,23 +101,36 @@ private void publishIntoCache(KVPair result, ExtensionConfig ex final Extension extension = result.getKey(); final String extensionIdWthEnv = extension.getEnv() + ":" + extension.getId(); final File jarFile = result.getValue(); + IExtensionInit initObj = null; ClassLoader loader; try { - loader = ClassLoaderHelper.getClassLoader(jarFile, extensionConfig.getWhilelistDelegateClass()); + @SuppressWarnings("unchecked") + List whitelistClasses = (List) extension + .getProperties() + .getOrDefault(WHITE_LIST_CLASSES, extensionConfig.getWhilelistDelegateClass()); + logger.info("Whitelist classes in use for extension {} are {}", extensionIdWthEnv, whitelistClasses); + loader = ClassLoaderHelper.getClassLoader(jarFile, whitelistClasses); } catch (Exception e) { - logger.error("Failed to load extension jar " + extension.getId(), e); + logger.error("Failed to load extension jar " + extensionIdWthEnv, e); return; } if (!isNullOrEmpty(extension.getInitClassName())) { try { Class clz = loader.loadClass(extension.getInitClassName()); - clz.getConstructor(INaksha.class, Extension.class).newInstance(naksha, extension); - } catch (ClassNotFoundException - | InvocationTargetException - | InstantiationException - | NoSuchMethodException - | IllegalAccessException e) { + Object obj = clz.getConstructor().newInstance(); + if (obj instanceof IExtensionInit initInstance) { + initInstance.init(naksha, extension); + initObj = initInstance; + logger.info( + "Extension {} initialization using initClassName {} done successfully.", + extensionIdWthEnv, + extension.getInitClassName()); + } else { + logger.error("InitClassName {} does not implement IExtensionInit for Extension {}", extension.getInitClassName(), extensionIdWthEnv); + return; + } + } catch (Exception e) { logger.error( "Failed to instantiate class {} for extension {} ", extension.getInitClassName(), @@ -128,28 +139,48 @@ private void publishIntoCache(KVPair result, ExtensionConfig ex return; } } - if (!isNullOrEmpty(extension.getInitClassName())) - logger.info( - "Extension {} initialization using initClassName {} done successfully.", - extensionIdWthEnv, - extension.getInitClassName()); - loaderCache.put(extensionIdWthEnv, new KVPair(extension, loader)); - PluginCache.removeExtensionCache(extensionIdWthEnv); + + ValueTuple previousValue = loaderCache.put(extensionIdWthEnv, new ValueTuple(extension, loader, initObj)); + if (previousValue != null) { + IExtensionInit previousInitObj = previousValue.getInstance(); + closeExtensionInstance(extensionIdWthEnv, previousInitObj); + } + logger.info( - "Extension id={}, version={} is successfully loaded into the cache, using Jar at {} for env={}.", + "Extension id={}, version={} is successfully loaded into the cache, using Jar at {}.", extensionIdWthEnv, extension.getVersion(), - extension.getUrl().substring(extension.getUrl().lastIndexOf("/") + 1), - extension.getEnv()); + extension.getUrl().substring(extension.getUrl().lastIndexOf("/") + 1)); + } + } + + private void removeExtensionFromCache(String extensionId) { + ValueTuple valueTuple = loaderCache.remove(extensionId); + PluginCache.removeExtensionCache(extensionId); + logger.info("Extension {} removed from cache.", extensionId); + if (valueTuple != null) { + IExtensionInit initObj = valueTuple.getInstance(); + closeExtensionInstance(extensionId, initObj); + } + } + + private void closeExtensionInstance(String extensionId, IExtensionInit initObj) { + if (initObj != null) { + try { + initObj.close(); + logger.info("Extension {} closed successfully.", extensionId); + } catch (Exception e) { + logger.error("Failed to close extension {}", extensionId, e); + } } } private boolean isLoaderMappingExist(Extension extension) { final String extensionIdWthEnv = extension.getEnv() + ":" + extension.getId(); - KVPair existingMapping = loaderCache.get(extensionIdWthEnv); + ValueTuple existingMapping = loaderCache.get(extensionIdWthEnv); if (existingMapping == null) return false; - final Extension exExtension = existingMapping.getKey(); + final Extension exExtension = existingMapping.getExtension(); final String exInitClassName = isNullOrEmpty(exExtension.getInitClassName()) ? "" : exExtension.getInitClassName(); final String initClassName = isNullOrEmpty(extension.getInitClassName()) ? "" : extension.getInitClassName(); @@ -160,7 +191,7 @@ private boolean isLoaderMappingExist(Extension extension) { } /** - * Lamda function which will initiate the downloading for extension jar + * Lambda function which will initiate the downloading for extension jar */ private KVPair downloadJar(Extension extension) { logger.info("Downloading jar {} with version {} ", extension.getId(), extension.getVersion()); @@ -184,8 +215,8 @@ protected FileClient getJarClient(String url) { } protected ClassLoader getClassLoaderById(@NotNull String extensionId) { - KVPair mappedLoader = loaderCache.get(extensionId); - return mappedLoader == null ? null : mappedLoader.getValue(); + ValueTuple mappedLoader = loaderCache.get(extensionId); + return mappedLoader == null ? null : mappedLoader.getClassLoader(); } public int getCacheLength() { @@ -193,7 +224,7 @@ public int getCacheLength() { } public List getCachedExtensions() { - return loaderCache.values().stream().map(KVPair::getKey).toList(); + return loaderCache.values().stream().map(ValueTuple::getExtension).toList(); } private boolean isNullOrEmpty(String value) { diff --git a/here-naksha-lib-ext-manager/src/main/java/com/here/naksha/lib/extmanager/ValueTuple.java b/here-naksha-lib-ext-manager/src/main/java/com/here/naksha/lib/extmanager/ValueTuple.java new file mode 100644 index 000000000..6eca51d76 --- /dev/null +++ b/here-naksha-lib-ext-manager/src/main/java/com/here/naksha/lib/extmanager/ValueTuple.java @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2017-2024 HERE Europe B.V. + * + * 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. + * + * SPDX-License-Identifier: Apache-2.0 + * License-Filename: LICENSE + */ +package com.here.naksha.lib.extmanager; + +import com.here.naksha.lib.core.IExtensionInit; +import com.here.naksha.lib.core.models.features.Extension; + +public class ValueTuple { + private final Extension extension; + private final ClassLoader classLoader; + private final IExtensionInit instance; + + public ValueTuple(Extension extension, ClassLoader classLoader, IExtensionInit instance) { + this.extension = extension; + this.classLoader = classLoader; + this.instance = instance; + } + + public Extension getExtension() { + return extension; + } + + public ClassLoader getClassLoader() { + return classLoader; + } + + public IExtensionInit getInstance() { + return instance; + } +}