From a351411c197ea01db1dae929e00af522ee6e01e3 Mon Sep 17 00:00:00 2001 From: Guillaume DE OLIVEIRA 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 + .../system-configuration.md | 5 + .../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 +- 15 files changed, 618 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/docs/manual/docs/administrator-guide/configuring-the-catalog/system-configuration.md b/docs/manual/docs/administrator-guide/configuring-the-catalog/system-configuration.md index 2bec134017e..bccead43eab 100644 --- a/docs/manual/docs/administrator-guide/configuring-the-catalog/system-configuration.md +++ b/docs/manual/docs/administrator-guide/configuring-the-catalog/system-configuration.md @@ -272,3 +272,8 @@ Allows to view metadata history ## Harvesting *Allow editing on harvested records*: Enables/Disables editing of harvested records in the catalogue. By default, harvested records cannot be edited. + +## Geonetwork-ui configuration + +*Enable datahub*: +You can adapt your GeoNetwork UI by enabling Datahub. First, download the specific plugin datahub-integration. Then, on the settings page, enable Datahub . you can provide an other specific Datahub configuration than the default one. You can also configure it for each portal. Ensure that Datahub is enabled in the settings to make it work on portals. 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 @@