Skip to content

Commit 5b9fb52

Browse files
committed
DS-1801: WIP
Signed-off-by: Benjamin Rögner <[email protected]>
1 parent 5ad727b commit 5b9fb52

File tree

9 files changed

+79
-80
lines changed

9 files changed

+79
-80
lines changed

xyz-hub-service/src/main/java/com/here/xyz/hub/rest/FeatureApi.java

Lines changed: 1 addition & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -590,45 +590,9 @@ private UpdateStrategy toUpdateStrategy(Space space, IfExists ifExists, IfNotExi
590590
);
591591
}
592592

593-
private String writeHook()
594-
{
595-
return // TODO: paths = [name, jsonpath] needs to be filled from index configuration
596-
"""
597-
feature => {
598-
599-
const paths = [
600-
["$alias1", "$.properties.name"],
601-
["$alias2", "$.properties.location.lat"],
602-
["$alias3", "$.properties.tags[*]"]
603-
];
604-
605-
function extractJsonPaths(feature, paths) {
606-
const result = {};
607-
608-
for (const [name, jsonPath] of paths) {
609-
try {
610-
const value = jp.query(feature, jsonPath);
611-
// decide whether to store arrays or single values:
612-
result[name] = value.length === 1 ? value[0] : value;
613-
} catch (err) {
614-
result[name] = null; // or undefined / error message
615-
console.error(`Error evaluating JSONPath for ${name}:`, err.message);
616-
}
617-
}
618-
619-
return result;
620-
}
621-
622-
return ["searchable",extractJsonPaths(feature, paths) ];
623-
}
624-
625-
""";
626-
}
627-
628593
private Set<Modification> toModifications(RoutingContext context, Space space, FeatureModificationList featureModificationList,
629594
boolean versionConflictDetectionEnabled) {
630595
List<String> featureHooks = new ArrayList<>();
631-
List<String> writeHooks = new ArrayList<>();
632596
List<String> addTags = getAddTags(context);
633597
List<String> removeTags = getRemoveTags(context);
634598
String idPrefix = getIdPrefix(context);
@@ -639,20 +603,13 @@ private Set<Modification> toModifications(RoutingContext context, Space space, F
639603
if (idPrefix != null)
640604
featureHooks.add("feature => feature.id = \"" + idPrefix + "\" + feature.id");
641605

642-
boolean useWriteHook = true;
643-
644-
if( useWriteHook )
645-
writeHooks.add(writeHook()); // todo: pass searchable aliases to use within writehook
646-
647606
return featureModificationList.getModifications().stream()
648607
.map(modification -> new Modification()
649608
.withFeatureData(modification.getFeatureData())
650609
.withUpdateStrategy(toUpdateStrategy(space, modification.getOnFeatureExists(), modification.getOnFeatureNotExists(),
651610
modification.getOnMergeConflict(), versionConflictDetectionEnabled))
652611
.withPartialUpdates(modification.getOnFeatureExists() == PATCH)
653-
.withFeatureHooks(featureHooks.isEmpty() ? null : featureHooks)
654-
.withWriteHooks(writeHooks.isEmpty() ? null : writeHooks)
655-
)
612+
.withFeatureHooks(featureHooks.isEmpty() ? null : featureHooks))
656613
.collect(Collectors.toSet());
657614
}
658615

xyz-hub-service/src/main/java/com/here/xyz/hub/task/FeatureHandler.java

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,8 @@
1919

2020
package com.here.xyz.hub.task;
2121

22-
import static com.here.xyz.hub.task.FeatureTaskHandler.injectMinVersion;
2322
import static com.here.xyz.hub.task.FeatureTask.resolveBranchFor;
23+
import static com.here.xyz.hub.task.FeatureTaskHandler.injectMinVersion;
2424
import static com.here.xyz.util.service.rest.TooManyRequestsException.ThrottlingReason.MEMORY;
2525
import static com.here.xyz.util.service.rest.TooManyRequestsException.ThrottlingReason.STORAGE_QUEUE_FULL;
2626
import static io.netty.handler.codec.http.HttpResponseStatus.BAD_GATEWAY;
@@ -65,6 +65,7 @@
6565
import java.util.concurrent.CompletableFuture;
6666
import java.util.concurrent.ConcurrentHashMap;
6767
import java.util.concurrent.atomic.LongAdder;
68+
import java.util.stream.Collectors;
6869
import net.jodah.expiringmap.ExpirationPolicy;
6970
import net.jodah.expiringmap.ExpiringMap;
7071
import org.apache.logging.log4j.LogManager;
@@ -95,7 +96,12 @@ public static Future<FeatureCollection> writeFeatures(Marker marker, Space space
9596
.withContext(spaceContext)
9697
.withAuthor(author)
9798
.withResponseDataExpected(responseDataExpected)
98-
.withRef(baseRef);
99+
.withRef(baseRef)
100+
.withSearchableProperties(space.getSearchableProperties().entrySet().stream()
101+
.collect(Collectors.toMap(
102+
e -> e.getKey().startsWith("$") ? e.getKey().substring(1) : e.getKey(),
103+
e -> e.getKey().startsWith("$") ? toJsonPath(e.getKey().substring(e.getKey().indexOf(":") + 1)) : toJsonPath(e.getKey())
104+
)));
99105

100106
return Future.all(injectMinVersion(marker, space.getId(), event), injectSpaceParams(event, space))
101107
.compose(v -> {
@@ -131,6 +137,15 @@ else if (ar.result() instanceof FeatureCollection featureCollection)
131137
}
132138
}
133139

140+
private static String toJsonPath(String jsonPathPointer) {
141+
if (jsonPathPointer.startsWith("$"))
142+
//The path pointer is already a JSONPath
143+
return jsonPathPointer;
144+
145+
//TODO: Translate the path pointer like "prop1.prop2[0].prop3" to JSONPath "$.prop1.prop2[0].prop3" for all cases correctly
146+
return "$." + jsonPathPointer;
147+
}
148+
134149
static void throttle(Space space) throws HttpException {
135150
Connector storage = space.getResolvedStorageConnector();
136151
final long GLOBAL_INFLIGHT_REQUEST_MEMORY_SIZE = (long) Service.configuration.GLOBAL_INFLIGHT_REQUEST_MEMORY_SIZE_MB * 1024 * 1024;

xyz-models/src/main/java/com/here/xyz/events/WriteFeaturesEvent.java

Lines changed: 21 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright (C) 2017-2024 HERE Europe B.V.
2+
* Copyright (C) 2017-2025 HERE Europe B.V.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -21,11 +21,18 @@
2121

2222
import com.here.xyz.models.geojson.implementation.FeatureCollection;
2323
import java.util.List;
24+
import java.util.Map;
2425
import java.util.Set;
2526

2627
public class WriteFeaturesEvent extends ContextAwareEvent<WriteFeaturesEvent> {
2728
private Set<Modification> modifications;
2829
private boolean responseDataExpected;
30+
/**
31+
* A map of properties that are searchable.
32+
* The key is the name of the alias / field that should be indexed.
33+
* The value is the JSON-Pointer to the property in the feature or a JSONPath expression.
34+
*/
35+
private Map<String, String> searchableProperties;
2936

3037
public Set<Modification> getModifications() {
3138
return modifications;
@@ -53,13 +60,25 @@ public WriteFeaturesEvent withResponseDataExpected(boolean responseDataExpected)
5360
return this;
5461
}
5562

63+
public Map<String, String> getSearchableProperties() {
64+
return searchableProperties;
65+
}
66+
67+
public void setSearchableProperties(Map<String, String> searchableProperties) {
68+
this.searchableProperties = searchableProperties;
69+
}
70+
71+
public WriteFeaturesEvent withSearchableProperties(Map<String, String> searchableProperties) {
72+
setSearchableProperties(searchableProperties);
73+
return this;
74+
}
75+
5676
public static class Modification {
5777
private UpdateStrategy updateStrategy;
5878
private FeatureCollection featureData;
5979
private List<String> featureIds; //To be used only for deletions
6080
private boolean partialUpdates;
6181
private List<String> featureHooks; //NOTE: The featureHooks will be applied in the given order
62-
private List<String> writeHooks; //NOTE: The Hooks will be applied in the given order
6382

6483
public UpdateStrategy getUpdateStrategy() {
6584
return updateStrategy;
@@ -125,20 +144,5 @@ public Modification withFeatureHooks(List<String> featureHooks) {
125144
setFeatureHooks(featureHooks);
126145
return this;
127146
}
128-
129-
public List<String> getWriteHooks() {
130-
return writeHooks;
131-
}
132-
133-
public void setWriteHooks(List<String> writeHooks) {
134-
this.writeHooks = writeHooks;
135-
}
136-
137-
public Modification withWriteHooks(List<String> writeHooks) {
138-
setWriteHooks(writeHooks);
139-
return this;
140-
}
141-
142-
143147
}
144148
}

xyz-psql-connector/src/main/java/com/here/xyz/psql/query/WriteFeatures.java

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@
4242
import java.sql.SQLException;
4343
import java.util.ArrayList;
4444
import java.util.List;
45+
import java.util.Map;
4546
import java.util.Set;
4647
import org.apache.logging.log4j.LogManager;
4748
import org.apache.logging.log4j.Logger;
@@ -96,6 +97,9 @@ protected SQLQuery buildQuery(WriteFeaturesEvent event) throws ErrorResponseExce
9697
if (event.getRef() != null && event.getRef().isSingleVersion() && !event.getRef().isHead())
9798
queryContextBuilder.withBaseVersion(event.getRef().getVersion());
9899

100+
if (event.getSearchableProperties() != null && !event.getSearchableProperties().isEmpty())
101+
queryContextBuilder.with("writeHooks", List.of(writeHook(event.getSearchableProperties())));
102+
99103
return new SQLQuery("SELECT write_features(#{modifications}, 'Modifications', #{author}, #{responseDataExpected});")
100104
.withLoggingEnabled(false)
101105
.withContext(queryContextBuilder.build())
@@ -134,4 +138,27 @@ public FeatureCollection handle(ResultSet rs) throws SQLException {
134138
throw new RuntimeException("Error parsing query result.", e);
135139
}
136140
}
141+
142+
private String writeHook(Map<String, String> searchableProperties) {
143+
return """
144+
feature => {
145+
const searchableProperties = ${searchableProperties};
146+
let searchables = {};
147+
148+
for (const alias in searchableProperties) {
149+
const jsonPath = searchableProperties[alias];
150+
try {
151+
searchables[alias] = jsonpath_rfc9535.query(feature, jsonPath)[0];
152+
}
153+
catch (err) {
154+
throw new Error(`Error evaluating JSONPath for alias ${alias} and expression ${jsonPath} message: ${err.message}`);
155+
}
156+
}
157+
158+
return {
159+
"searchable": searchables
160+
};
161+
}
162+
""".replace("${searchableProperties}", XyzSerializable.serialize(searchableProperties));
163+
}
137164
}

xyz-util/src/main/java/com/here/xyz/util/db/pg/Script.java

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -171,10 +171,6 @@ private void install(String targetSchema, boolean deleteBefore) throws SQLExcept
171171
logger.info("Installing script {} on DB {} into schema {} ...", getScriptName(), getDbId(), targetSchema);
172172

173173
SQLQuery scriptContent = loadSubstitutedScriptContent();
174-
175-
if( "common.sql".equals(getScriptName()) )
176-
scriptContent = addJsLibRegisterFunctions(scriptContent);
177-
178174
List<SQLQuery> installationQueries = new ArrayList<>();
179175
if (deleteBefore) {
180176
//TODO: Remove following workaround once "drop schema cascade"-bug creating orphaned functions is fixed in postgres
@@ -315,9 +311,7 @@ private static List<String> scanResourceFolderWA(String resourceFolder, String f
315311
private static List<String> scanResourceFolder(ScriptResourcePath scriptResourcePath, String fileSuffix) throws IOException {
316312
String resourceFolder = scriptResourcePath.path();
317313
//TODO: Remove this workaround once the actual implementation of this method supports scanning folders inside a JAR
318-
if ( "/sql".equals(resourceFolder)
319-
|| "/sql/lib-js".equals(resourceFolder)
320-
|| "/jobs".equals(resourceFolder))
314+
if ("/sql".equals(resourceFolder) || "/jobs".equals(resourceFolder))
321315
return ensureInitScriptIsFirst(scanResourceFolderWA(resourceFolder, fileSuffix), scriptResourcePath.initScript());
322316

323317
final InputStream folderResource = Script.class.getResourceAsStream(resourceFolder);

xyz-util/src/main/resources/sql/DatabaseWriter.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -314,6 +314,12 @@ class DatabaseWriter {
314314
return this.execute()[0];
315315
}
316316

317+
createJsonData(inputFeature) {
318+
let writeHooks = queryContext().writeHooks;
319+
//TODO: Translate the inputFeature into the value for the jsondata column (also apply the write hooks here if some are specified)
320+
return inputFeature;
321+
}
322+
317323
_PARTITION_SIZE() {
318324
return queryContext().PARTITION_SIZE || 100000; //TODO: Ensure the partition size is always set in the query context
319325
}

xyz-util/src/main/resources/sql/FeatureWriter.js

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,6 @@ class FeatureWriter {
4343
isPartial;
4444
baseVersion;
4545
featureHooks;
46-
writeHooks;
4746

4847
onExists;
4948
onNotExists;
@@ -906,11 +905,11 @@ class FeatureWriter {
906905
/**
907906
* @returns {FeatureCollection}
908907
*/
909-
static writeFeatures(inputFeatures, author, onExists, onNotExists, onVersionConflict, onMergeConflict, isPartial, featureHooks, writeHooks, version = FeatureWriter.getNextVersion()) {
908+
static writeFeatures(inputFeatures, author, onExists, onNotExists, onVersionConflict, onMergeConflict, isPartial, featureHooks, version = FeatureWriter.getNextVersion()) {
910909
FeatureWriter.dbWriter = new DatabaseWriter(queryContext().schema, FeatureWriter._targetTable(), FeatureWriter._tableBaseVersions().at(-1), FW_BATCH_MODE(), queryContext().tableLayout);
911910
let result = this.newFeatureCollection();
912911
for (let feature of inputFeatures) {
913-
let execution = new FeatureWriter(feature, version, author, onExists, onNotExists, onVersionConflict, onMergeConflict, isPartial, featureHooks, writeHooks).writeFeature();
912+
let execution = new FeatureWriter(feature, version, author, onExists, onNotExists, onVersionConflict, onMergeConflict, isPartial, featureHooks).writeFeature();
914913
this._collectResult(execution, result);
915914
}
916915

@@ -952,18 +951,16 @@ class FeatureWriter {
952951
let featureCollections = featureModifications.map(modification => FeatureWriter.writeFeatures(this.toFeatureList(modification),
953952
author, modification.updateStrategy.onExists, modification.updateStrategy.onNotExists,
954953
modification.updateStrategy.onVersionConflict, modification.updateStrategy.onMergeConflict, modification.partialUpdates,
955-
modification.featureHooks && modification.featureHooks.map(hook => eval(hook)),
956-
modification.writeHooks && modification.writeHooks.map(hook => eval(hook)),
957-
version));
954+
modification.featureHooks && modification.featureHooks.map(hook => eval(hook)), version));
958955
return this.combineResults(featureCollections);
959956
}
960957

961958
/**
962959
* @returns {FeatureCollection}
963960
*/
964-
static writeFeature(inputFeature, author, onExists, onNotExists, onVersionConflict, onMergeConflict, isPartial, featureHooks, writeHooks, version = undefined) {
961+
static writeFeature(inputFeature, author, onExists, onNotExists, onVersionConflict, onMergeConflict, isPartial, featureHooks, version = undefined) {
965962
return FeatureWriter.writeFeatures([inputFeature], author, onExists, onNotExists, onVersionConflict, onMergeConflict,
966-
isPartial, featureHooks, writeHooks, version);
963+
isPartial, featureHooks, version);
967964
}
968965
}
969966

xyz-util/src/main/resources/sql/lib-js/sample_hello.min.js

Lines changed: 0 additions & 1 deletion
This file was deleted.

xyz-util/src/test/js/db/featurewriter/TestFeatureWriter.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ class TestFeatureWriter {
8888
}
8989

9090
run() {
91-
let result = FeatureWriter.writeFeature(this.inputFeature, null, "REPLACE", "CREATE", null, null, false, null, null);
91+
let result = FeatureWriter.writeFeature(this.inputFeature, null, "REPLACE", "CREATE", null, null, false, null);
9292
console.log("Returned result from FeatureWriter: ", result);
9393
}
9494

0 commit comments

Comments
 (0)