From 30d9db801a933ca7af09890d9c1281d186d3b877 Mon Sep 17 00:00:00 2001 From: Romuald Caplier Date: Wed, 12 Jun 2024 10:20:25 +0200 Subject: [PATCH] feat(geonetwork/web): Add datahub integration. --- .gitignore | 4 + .../fao/geonet/kernel/setting/Settings.java | 3 + .../java/org/fao/geonet/domain/Source.java | 27 +++ plugins/datahub-integration/pom.xml | 201 ++++++++++++++++++ .../fao/geonet/datahub/DatahubController.java | 181 ++++++++++++++++ .../org/fao/geonet/datahub/FileUtils.java | 61 ++++++ pom.xml | 1 + .../fao/geonet/api/sources/SourcesApi.java | 2 + .../catalog/js/admin/SourcesController.js | 4 +- .../templates/admin/settings/sources.html | 21 ++ .../templates/admin/settings/system.html | 17 ++ web/pom.xml | 7 + .../sql/migrate/v447/migrate-default.sql | 78 +++++++ .../config-security-mapping.xml | 8 +- 14 files changed, 613 insertions(+), 2 deletions(-) create mode 100644 plugins/datahub-integration/pom.xml create mode 100644 plugins/datahub-integration/src/main/java/org/fao/geonet/datahub/DatahubController.java create mode 100644 plugins/datahub-integration/src/main/java/org/fao/geonet/datahub/FileUtils.java mode change 100644 => 100755 web-ui/src/main/resources/catalog/templates/admin/settings/system.html diff --git a/.gitignore b/.gitignore index 84f282d0af7..08e0dd5c5fb 100644 --- a/.gitignore +++ b/.gitignore @@ -88,3 +88,7 @@ web/src/main/webapp/data/ web/src/main/webapp/doc/en web/src/main/webapp/doc/fr web/src/main/webapp/WEB-INF/data/data/resources/schemapublication + +# geonetwork-ui git project +plugins/datahub-integration/src/main/geonetwork-ui/ +plugins/datahub-integration/node/ diff --git a/core/src/main/java/org/fao/geonet/kernel/setting/Settings.java b/core/src/main/java/org/fao/geonet/kernel/setting/Settings.java index c3c7a9e2aa0..4cbefcd6c0d 100644 --- a/core/src/main/java/org/fao/geonet/kernel/setting/Settings.java +++ b/core/src/main/java/org/fao/geonet/kernel/setting/Settings.java @@ -166,6 +166,9 @@ public class Settings { public static final String MICROSERVICES_ENABLED = "microservices/enabled"; + public static final String GEONETWORK_UI_DATAHUB_CONFIGURATION = "geonetwork-ui/datahub/configuration"; + public static final String GEONETWORK_UI_DATAHUB_ENABLED = "geonetwork-ui/datahub/enabled"; + public static class GNSetting { private String name; private boolean nullable; diff --git a/domain/src/main/java/org/fao/geonet/domain/Source.java b/domain/src/main/java/org/fao/geonet/domain/Source.java index 7fb337745dc..791464d69f9 100644 --- a/domain/src/main/java/org/fao/geonet/domain/Source.java +++ b/domain/src/main/java/org/fao/geonet/domain/Source.java @@ -27,6 +27,7 @@ import org.fao.geonet.domain.converter.BooleanToYNConverter; import org.fao.geonet.entitylistener.SourceEntityListenerManager; import org.fao.geonet.repository.LanguageRepository; +import org.hibernate.annotations.Type; import javax.annotation.Nonnull; import javax.persistence.*; @@ -64,6 +65,9 @@ public class Source extends Localized { private Integer groupOwner; private Boolean listableInHeaderSelector = true; + private Boolean datahubEnabled = false; + private String datahubConfiguration = ""; // will use the main conf if empty + /** * Default constructor. Required by framework. */ @@ -224,6 +228,29 @@ public Source setUiConfig(String uiConfig) { return this; } + /** + * Only applies to subportal. + * + * @return + */ + public Boolean getDatahubEnabled() { + return datahubEnabled; + } + public Source setDatahubEnabled(Boolean datahubEnabled) { + this.datahubEnabled = datahubEnabled; + return this; + } + + @Lob + @Type(type = "org.hibernate.type.TextType") + public String getDatahubConfiguration() { + return datahubConfiguration; + } + public Source setDatahubConfiguration(String datahubConfiguration) { + this.datahubConfiguration = datahubConfiguration; + return this; + } + /** * Get the date that the source was created. diff --git a/plugins/datahub-integration/pom.xml b/plugins/datahub-integration/pom.xml new file mode 100644 index 00000000000..af7f1800dca --- /dev/null +++ b/plugins/datahub-integration/pom.xml @@ -0,0 +1,201 @@ + + + + + + geonetwork + org.geonetwork-opensource + 4.4.7-SNAPSHOT + + 4.0.0 + + + org.geonetwork-opensource.plugins + gn-datahub-integration + GeoNetwork Datahub integration + jar + + + + org.geonetwork-opensource + gn-core + ${project.version} + provided + + + + org.springframework + spring-context + provided + + + + org.springframework + spring-context-support + provided + + + + + + datahub-integration + + + + release + + + + gn-datahub-integration + + + + + + main + ../.. + + + + + + + org.codehaus.mojo + exec-maven-plugin + 3.3.0 + + + delete-existing + initialize + + exec + + + rm + + -rf + src/main/geonetwork-ui + + + + + checkout-geonetwork-ui + initialize + + exec + + + git + + clone + --branch + ${geonetwork-ui.git.branch} + https://github.com/geonetwork/geonetwork-ui.git + src/main/geonetwork-ui + + + + + + + + + com.github.eirslett + frontend-maven-plugin + 1.15.0 + + + + install-node-and-npm + + install-node-and-npm + + + v20.12.2 + 10.7.0 + + + + + npm-install + + npm + + + ci --loglevel error + src/main/geonetwork-ui + ${basedir} + + + + + npm-build + + npm + + + + false + + + run nx -- build datahub --base-href=./ + + src/main/geonetwork-ui + ${basedir} + + + + + + + + maven-resources-plugin + 3.1.0 + + + copy-resources + generate-resources + + copy-resources + + + true + UTF-8 + src/main/resources/datahub + + + src/main/geonetwork-ui/dist/apps/datahub + false + + + + + + + + + + diff --git a/plugins/datahub-integration/src/main/java/org/fao/geonet/datahub/DatahubController.java b/plugins/datahub-integration/src/main/java/org/fao/geonet/datahub/DatahubController.java new file mode 100644 index 00000000000..eb979748c87 --- /dev/null +++ b/plugins/datahub-integration/src/main/java/org/fao/geonet/datahub/DatahubController.java @@ -0,0 +1,181 @@ +package org.fao.geonet.datahub; + +import org.apache.commons.io.IOUtils; +import org.apache.http.HttpHeaders; +import org.fao.geonet.NodeInfo; +import org.fao.geonet.domain.Source; +import org.fao.geonet.domain.SourceType; +import org.fao.geonet.kernel.setting.SettingManager; +import org.fao.geonet.kernel.setting.Settings; +import org.fao.geonet.repository.SourceRepository; +import org.fao.geonet.utils.Log; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.servlet.view.RedirectView; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import java.io.*; +import java.nio.file.Files; +import java.util.Objects; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import java.util.zip.GZIPOutputStream; + +import static org.fao.geonet.kernel.schema.SchemaPlugin.LOGGER_NAME; + +@RequestMapping(value = {"/{geonetworkPath:[a-zA-Z0-9_\\-]+}"}) +@Controller("datahub") +public class DatahubController { + + @Autowired + SourceRepository sourceRepository; + + @Autowired + SettingManager settingManager; + + @GetMapping("/datahub") + public RedirectView redirectDatahub(HttpServletRequest request, HttpServletResponse response) { + String uri = request.getRequestURI(); + if (!uri.endsWith("/")) { + uri += "/"; + } + return new RedirectView(uri + "index.html"); + } + + @GetMapping("/{locale:[a-z]{2,3}}/datahub") + public RedirectView redirectLocalizedDatahub(HttpServletRequest request, HttpServletResponse response) { + String uri = request.getRequestURI(); + if (!uri.endsWith("/")) { + uri += "/"; + } + return new RedirectView(uri + "index.html"); + } + + @RequestMapping("/datahub/**") + public void handleDatahubWithFilepath(HttpServletRequest request, HttpServletResponse response) throws IOException { + handleDatahubRequest(request, response,null); + } + + @RequestMapping("/{locale:[a-z]{2,3}}/datahub/**") + public void handleLocalizedDatahubWithFilepath(HttpServletRequest request, HttpServletResponse response, @PathVariable String locale) throws IOException { + handleDatahubRequest(request, response, locale); + } + + void handleDatahubRequest(HttpServletRequest request, HttpServletResponse response,String locale) throws IOException { + Log.debug(LOGGER_NAME, "enter in datahub"); + + if (!isDatahubEnabled()) { + response.setStatus(HttpServletResponse.SC_NOT_FOUND); + return; + } + + String portalName = getPortalName(request); + if (!isPortalDatahubEnabled(portalName)) { + response.setStatus(HttpServletResponse.SC_NOT_FOUND); + return; + } + + File actualFile = getRequestedFile(request, locale); + if (!actualFile.exists()) { + actualFile = getFallbackFile(); + disableCacheForIndex(response); + } + + setResponseHeaders(response, actualFile); + writeResponseContent(request, response, actualFile, portalName); + } + + private boolean isDatahubEnabled() { + return Objects.equals(settingManager.getValue(Settings.GEONETWORK_UI_DATAHUB_ENABLED), "true"); + } + + private String getPortalName(HttpServletRequest request) { + String reqPath = request.getPathInfo(); + String[] parts = reqPath.split("/"); + return parts[1]; + } + + private boolean isPortalDatahubEnabled(String portalName) { + if (NodeInfo.DEFAULT_NODE.equals(portalName)) { + return isDatahubEnabled(); + } else if (sourceRepository.existsByUuidAndType(portalName, SourceType.subportal)) { + return Objects.requireNonNull(sourceRepository.findOneByUuid(portalName)).getDatahubEnabled(); + } + return false; + } + + private File getRequestedFile(HttpServletRequest request, String locale) { + String reqPath = request.getPathInfo(); + int pathPartToSkip = 3;// "/srv/datahub/bla/bla" + if (locale != null) { + pathPartToSkip = 4;// /srv/fre/datahub/bla/bla + } + + String filePath = Stream.of(reqPath.split("/")).skip(pathPartToSkip).collect(Collectors.joining("/")); + if (!FileUtils.fileExistsInJar("/datahub/" + filePath)) { + return new File(filePath); + } + try { + return FileUtils.getFileFromJar("/datahub/" + filePath); + } catch (IOException e) { + Log.error(LOGGER_NAME, e.getMessage()); + return new File(filePath); + } + } + + private File getFallbackFile() { + String indexPath = "/datahub/index.html"; + try{ + return FileUtils.getFileFromJar(indexPath); + } catch (IOException e) { + Log.error(LOGGER_NAME, e.getMessage()); + return new File(indexPath); + } + } + + private void disableCacheForIndex(HttpServletResponse response) { + response.setHeader(HttpHeaders.CACHE_CONTROL, "no-cache"); + response.setHeader(HttpHeaders.PRAGMA, "no-cache"); + response.setHeader(HttpHeaders.EXPIRES, "0"); + } + + private void setResponseHeaders(HttpServletResponse response, File actualFile) throws IOException { + response.setStatus(HttpServletResponse.SC_OK); + String extension = actualFile.getName().toLowerCase(); + String contentType = extension.equals("js") ? "text/javascript; charset=UTF-8" : Files.probeContentType(actualFile.toPath()); + response.setContentType(contentType); + } + + void writeResponseContent(HttpServletRequest request, HttpServletResponse response, File actualFile, String portalName) throws IOException { + InputStream inStream = actualFile.getName().equals("default.toml") ? readConfiguration(portalName) : new FileInputStream(actualFile); + OutputStream outStream = response.getOutputStream(); + + if (request.getHeader(HttpHeaders.ACCEPT_ENCODING).contains("gzip")) { + response.setHeader(HttpHeaders.CONTENT_ENCODING, "gzip"); + outStream = new GZIPOutputStream(outStream); + } + + IOUtils.copy(inStream, outStream); + outStream.close(); + } + InputStream readConfiguration(String portalName) { + String configuration = settingManager.getValue(Settings.GEONETWORK_UI_DATAHUB_CONFIGURATION); + + if (!portalName.equals(NodeInfo.DEFAULT_NODE)) { + Source portal = sourceRepository.findOneByUuid(portalName); + if (portal != null && !portal.getDatahubConfiguration().isEmpty()) { + configuration = portal.getDatahubConfiguration(); + } + } + + // remove url & add new one + configuration = configuration.replaceAll("\ngeonetwork4_api_url\\s?=.+", "\n") + .replace("[global]", "[global]\ngeonetwork4_api_url = \"/geonetwork/" + portalName + "/api\""); + return new ByteArrayInputStream(configuration.getBytes()); + } +} diff --git a/plugins/datahub-integration/src/main/java/org/fao/geonet/datahub/FileUtils.java b/plugins/datahub-integration/src/main/java/org/fao/geonet/datahub/FileUtils.java new file mode 100644 index 00000000000..b45f5dbda1f --- /dev/null +++ b/plugins/datahub-integration/src/main/java/org/fao/geonet/datahub/FileUtils.java @@ -0,0 +1,61 @@ +package org.fao.geonet.datahub; + +import org.springframework.core.io.ClassPathResource; + +import java.io.*; +import java.nio.file.Files; +import java.nio.file.Path; + +// Generated by ChatGPT +public class FileUtils { + private static final Path TEMP_DIR; + + static { + try { + TEMP_DIR = Files.createTempDirectory("plugin_cache"); + TEMP_DIR.toFile().deleteOnExit(); // Ensure temp directory is cleaned up + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + public static boolean fileExistsInJar(String relativePath) { + ClassPathResource resource = new ClassPathResource(relativePath); + return resource.isReadable(); + } + + public static File getFileFromJar(String relativePath) throws IOException { + File tempFile = new File(TEMP_DIR.toFile(), new File(relativePath).getName()); + if (tempFile.exists()) { + return tempFile; + } + + if (!FileUtils.fileExistsInJar(relativePath)) { + throw new IOException("File not found: " + relativePath); + } + + ClassPathResource resource = new ClassPathResource(relativePath); + + // Copy resource contents to temp file + try (InputStream inputStream = resource.getInputStream(); + FileOutputStream outputStream = new FileOutputStream(tempFile)) { + inputStream.transferTo(outputStream); + } + + tempFile.deleteOnExit(); // Mark for deletion on JVM exit + return tempFile; + } + + public static String readFromInputStream(InputStream inputStream) + throws IOException { + StringBuilder resultStringBuilder = new StringBuilder(); + try (BufferedReader br + = new BufferedReader(new InputStreamReader(inputStream))) { + String line; + while ((line = br.readLine()) != null) { + resultStringBuilder.append(line).append("\n"); + } + } + return resultStringBuilder.toString(); + } +} diff --git a/pom.xml b/pom.xml index cca4ec918be..77a84542c59 100644 --- a/pom.xml +++ b/pom.xml @@ -1425,6 +1425,7 @@ datastorages translationproviders auditable + plugins/datahub-integration diff --git a/services/src/main/java/org/fao/geonet/api/sources/SourcesApi.java b/services/src/main/java/org/fao/geonet/api/sources/SourcesApi.java index 5d53043290b..9edd5a11f27 100644 --- a/services/src/main/java/org/fao/geonet/api/sources/SourcesApi.java +++ b/services/src/main/java/org/fao/geonet/api/sources/SourcesApi.java @@ -356,6 +356,8 @@ private void updateSource(String sourceIdentifier, entity.setGroupOwner(source.getGroupOwner()); entity.setServiceRecord(source.getServiceRecord()); entity.setUiConfig(source.getUiConfig()); + entity.setDatahubEnabled(source.getDatahubEnabled()); + entity.setDatahubConfiguration(source.getDatahubConfiguration()); entity.setLogo(source.getLogo()); entity.setListableInHeaderSelector(source.isListableInHeaderSelector()); Map labelTranslations = source.getLabelTranslations(); diff --git a/web-ui/src/main/resources/catalog/js/admin/SourcesController.js b/web-ui/src/main/resources/catalog/js/admin/SourcesController.js index c83ff220c7c..930c140dabd 100644 --- a/web-ui/src/main/resources/catalog/js/admin/SourcesController.js +++ b/web-ui/src/main/resources/catalog/js/admin/SourcesController.js @@ -133,7 +133,9 @@ filter: "", serviceRecord: null, groupOwner: null, - listableInHeaderSelector: true + listableInHeaderSelector: true, + datahubEnabled: false, + datahubConfiguration: "" }; // TODO: init labels }; diff --git a/web-ui/src/main/resources/catalog/templates/admin/settings/sources.html b/web-ui/src/main/resources/catalog/templates/admin/settings/sources.html index 56edf2a2bb6..fb73fe67620 100644 --- a/web-ui/src/main/resources/catalog/templates/admin/settings/sources.html +++ b/web-ui/src/main/resources/catalog/templates/admin/settings/sources.html @@ -234,6 +234,27 @@

subPortalGroupOwnerHelp

+ + +

sourceDatahubEnabled-help

+ + + + +

sourceDatahubConfiguration-help

diff --git a/web-ui/src/main/resources/catalog/templates/admin/settings/system.html b/web-ui/src/main/resources/catalog/templates/admin/settings/system.html old mode 100644 new mode 100755 index cb22bbd21c2..1639cea26f9 --- a/web-ui/src/main/resources/catalog/templates/admin/settings/system.html +++ b/web-ui/src/main/resources/catalog/templates/admin/settings/system.html @@ -777,6 +777,23 @@