Metadata catalog for Deep-time Digital Earth (DDE), built on Esri Geoportal Server. Replaces a GeoNetwork 4.4.9 fork that required 100+ merge conflicts per upstream upgrade.
All DDE customizations live outside the base platform as config files, XSLTs, and a plugin JAR. Upgrading Geoportal Server is a WAR swap.
┌─────────────────────────────┐
│ Geoportal Catalog v3.0.2 │
Browser ──────│ (Tomcat 9) │
│ │
│ Search UI, Editor, CSW 2/3 │
│ ISO 19115-3 (built-in) │
└──────────┬──────────────────┘
│
┌─────────▼──────────┐
│ Elasticsearch │
│ 8.14.3 │
│ (sole datastore) │
└─────────▲──────────┘
│
┌─────────────────────────┴──────────────────┐
│ Geoportal Harvester v3.0.0 │
│ (Tomcat 9) │
│ │
│ Built-in: WAF, CSW, CKAN, OAI-PMH, DCAT, │
│ STAC, THREDDS, ArcGIS, JDBC │
│ Custom: CDIF Sitemap (JSON-LD) │
│ fromJsonCdif.xsl conversion │
└────────────────────────────────────────────┘
docker compose up -dElasticsearch starts first (healthcheck), then the catalog and harvester. Three containers total.
| Service | URL | Credentials |
|---|---|---|
| Catalog UI | http://localhost:8082/geoportal | gptadmin / gptadmin |
| Catalog REST API | http://localhost:8082/geoportal/rest/metadata/search | |
| Harvester UI | http://localhost:8083/harvester | |
| Harvester REST API | http://localhost:8083/harvester/rest/harvester/connectors/inbound | |
| Elasticsearch | http://localhost:9202 | |
| Kibana (debug) | docker compose --profile debug up kibana then http://localhost:5601 |
Upload a test record:
curl -X PUT -u gptadmin:gptadmin \
-H "Content-Type: application/xml" \
-d @test/sample-iso19115-3.xml \
http://localhost:8082/geoportal/rest/metadata/item/test-dde-001The CDIF connector JAR is volume-mounted into the harvester container. To rebuild it after code changes:
Prerequisites (one-time): clone and build the Geoportal Harvester SDK:
git clone https://github.com/Esri/geoportal-server-harvester.git
cd geoportal-server-harvester && git checkout v3.0.0
mvn clean install -DskipTestsBuild the connector and restart the harvester:
cd cdif-connector && mvn clean package
docker compose up -d geoportal-harvester --force-recreateThe shaded JAR and custom hrv-beans.xml are mounted into the harvester via docker-compose.yml.
- Open http://localhost:8083/harvester
- Create a new task with Input = CDIF Sitemap (JSON-LD)
- Set Sitemap URL to a CDIF sitemap (e.g.
http://geoportal-catalog:8080/geoportal/test/cdif-sitemap-local.xml) - Set Output = Geoportal Server 3.x with URL
http://geoportal-catalog:8080/geoportal, usernamegptadmin, passwordgptadmin - Execute -- records are converted from JSON-LD to ISO 19115-3 and published to the catalog
# Create a harvest task
curl -X POST http://localhost:8083/harvester/rest/harvester/tasks \
-H "Content-Type: application/json" \
-d '{
"name": "CDIF Sitemap to Geoportal",
"source": {
"type": "CDIF-SITEMAP",
"label": "",
"properties": {
"cdif-sitemap-url": "http://geoportal-catalog:8080/geoportal/test/cdif-sitemap-local.xml"
}
},
"destinations": [{
"action": {
"type": "GPT",
"label": "",
"properties": {
"gpt-host-url": "http://geoportal-catalog:8080/geoportal",
"cred-username": "gptadmin",
"cred-password": "gptadmin"
}
}
}]
}'
# Response includes {"uuid":"<TASK_UUID>", ...}
# Execute the task
curl -X POST http://localhost:8083/harvester/rest/harvester/tasks/<TASK_UUID>/execute
# Monitor progress
curl http://localhost:8083/harvester/rest/harvester/processesTested with 77 CDIF JSON-LD records from the CDIF validation suite -- all 77 harvested successfully with 0 failures.
# Export from GeoNetwork as ISO 19115-3
./scripts/export-geonetwork-records.sh http://localhost:8080/geonetwork ./exported-records
# Import into Geoportal
./scripts/import-to-geoportal.sh ./exported-records http://localhost:8082/geoportal gptadmin gptadmingeoportal-dde/
├── docker-compose.yml # Full stack: ES + Catalog + Harvester (+Kibana)
├── catalog/
│ ├── Dockerfile # Tomcat 9 + Catalog WAR v3.0.2
│ ├── app-context.xml # (unused -- config via env vars instead)
│ └── elastic-config.xml # (unused)
├── harvester/
│ ├── Dockerfile # Tomcat 9 + Harvester WAR v3.0.0
│ ├── hrv-beans.xml # Spring beans with CDIF connector registered
│ ├── hrv-beans-cdif.xml # Standalone CDIF-only bean definition
│ └── hrv-config.xml # (unused -- harvester configured via UI)
├── cdif-connector/ # Custom Geoportal Harvester plugin
│ ├── pom.xml # Maven build (shaded JAR with JDOM2, json, Saxon-HE, xmlresolver)
│ └── src/main/java/.../cdif/
│ ├── CdifConstants.java # Property key constants
│ ├── CdifSitemapConnector.java # InputConnector factory (UI template, validation)
│ ├── CdifSitemapBroker.java # InputBroker (sitemap parse, JSON-LD fetch, XSLT)
│ └── CdifSitemapDefinitionAdaptor.java # Typed config wrapper
├── xslt/
│ ├── fromJsonCdif.xsl # CDIF JSON-LD intermediate XML -> ISO 19115-3
│ └── iso19115-3-to-schemaorg.xsl # ISO 19115-3 -> schema.org JSON-LD
├── scripts/
│ ├── export-geonetwork-records.sh # Export from GeoNetwork via CSW
│ ├── import-to-geoportal.sh # Bulk import via REST API
│ ├── test-upload-record.sh # Upload a single test record
│ └── verify-deployment.sh # Smoke test all endpoints
├── test/
│ ├── sample-iso19115-3.xml # Test ISO 19115-3 record
│ ├── cdif-sitemap.xml # Sitemap pointing to GitHub raw URLs
│ └── cdif-sitemap-local.xml # Sitemap pointing to locally-mounted JSON-LD files
└── pygeoapi-config.yml # OGC API Records config (future)
The catalog uses environment variables for configuration (no config file overrides needed):
| Variable | Default | Description |
|---|---|---|
es_node |
(empty -- connects to localhost) | Elasticsearch hostname (not a URL) |
harvester_node |
(empty) | Harvester REST API URL |
gpt_authentication |
authentication-simple.xml |
Auth method |
gpt_indexName |
metadata |
ES index name |
These are set in docker-compose.yml under the geoportal-catalog service.
The CDIF connector (cdif-connector/) is a Geoportal Harvester InputBroker that:
- Fetches a sitemap XML from a configured URL
- Parses
<url>/<loc>elements (handles<sitemapindex>recursion) - For each URL: fetches JSON-LD, converts to intermediate XML via
org.json.XML - Normalizes all namespace-prefixed keys (
schema:name->schema_name,prov:wasGeneratedBy->prov_wasGeneratedBy,@id->id, etc.) - Generates a stable UUID from the
@idfield via SHA-1 - Applies
fromJsonCdif.xsl(XSLT 2.0 via Saxon-HE) to produce ISO 19115-3 - Yields the result as a
SimpleDataReferencefor the Geoportal OutputBroker
Known namespace prefixes in CDIF data: schema, prov, dcat, dcterms, cdi, ada, spdx, xas. All are normalized to underscores before XSLT processing.
The sitemap parsing is ported from GeoNetwork's simpleurl.Harvester using direct JDOM child traversal (avoids XPath issues with detached elements). The XSLT is a direct copy of GeoNetwork's fromJsonCdif.xsl, proven with 77 CDIF records.
The connector is bundled as a shaded JAR (~6.6 MB) containing JDOM2, org.json, Saxon-HE, and xmlresolver. The shade plugin excludes JAR signature files and the Saxon TransformerFactory SPI registration to avoid conflicts with the harvester's built-in XSLT processor.
| Format | How to access |
|---|---|
| ISO 19115-3 XML | Native storage format. GET /rest/metadata/item/{id} |
| ISO 19139 XML | CSW with outputSchema=http://www.isotc211.org/2005/gmd |
| schema.org JSON-LD | Via xslt/iso19115-3-to-schemaorg.xsl transform |
| Asset | Status |
|---|---|
fromJsonCdif.xsl (CDIF -> ISO 19115-3) |
Direct copy, proven with 77 records |
| Sitemap parsing logic (JDOM traversal) | Ported to CdifSitemapBroker |
| ISO 19115-3 support | Built into Geoportal v3.0.2 (no custom profile needed) |
| ES 8.x compatibility | Native in Geoportal (no patches needed) |
- GeoNetwork monolith fork (~1GB repo, 100-conflict merges per upgrade)
- All SMR-Samsung backup files (200+ files)
- Custom patches to GeoNetwork core Java code
- 40-module Maven build
Add a pygeoapi container reading from the same Elasticsearch index:
# Add to docker-compose.yml
pygeoapi:
image: geopython/pygeoapi:latest
ports:
- "5000:80"
volumes:
- ./pygeoapi-config.yml:/pygeoapi/local.config.ymlA skeleton config is provided in pygeoapi-config.yml.