diff --git a/.idea/jsonSchemas.xml b/.idea/jsonSchemas.xml index f30cb99ea9..7fb8f08e61 100644 --- a/.idea/jsonSchemas.xml +++ b/.idea/jsonSchemas.xml @@ -34,6 +34,11 @@ diff --git a/.vscode/settings.json b/.vscode/settings.json index 9eccf121c9..74a5770872 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -6,6 +6,9 @@ "java.sources.organizeImports.starThreshold": 999, "java.saveActions.organizeImports": true, "yaml.schemas": { - "./planetiler-custommap/planetiler.schema.json": "planetiler-custommap/**/*.yml" + "./planetiler-custommap/planetiler.schema.json": [ + "planetiler-custommap/**/*.y*ml", + "planetiler-experimental/**/*.y*ml" + ] } } diff --git a/NOTICE.md b/NOTICE.md index 25f81916d1..7257cb70d5 100644 --- a/NOTICE.md +++ b/NOTICE.md @@ -4,7 +4,7 @@ Planetiler licensed under the Apache license, Version 2.0 Copyright 2021 Michael Barry and Planetiler Contributors. -The `planetiler-core` module includes the following software: +Planetiler includes the following software: - Maven Dependencies: - Jackson for JSON/XML handling (Apache license) @@ -29,6 +29,8 @@ The `planetiler-core` module includes the following software: - org.snakeyaml:snakeyaml-engine (Apache license) - org.commonmark:commonmark (BSD 2-clause license) - org.tukaani:xz (public domain) + - org.luaj:luaj-jse (MIT license) + - org.apache.bcel:bcel (Apache license) - Adapted code: - `DouglasPeuckerSimplifier` from [JTS](https://github.com/locationtech/jts) (EDL) - `OsmMultipolygon` from [imposm3](https://github.com/omniscale/imposm3) (Apache license) @@ -48,6 +50,7 @@ The `planetiler-core` module includes the following software: - `SeekableInMemoryByteChannel` from [Apache Commons compress](https://commons.apache.org/proper/commons-compress/apidocs/org/apache/commons/compress/utils/SeekableInMemoryByteChannel.html) ( Apache License) + - Several classes in `org.luaj.vm2.*` from [luaj](https://github.com/luaj/luaj) (MIT License) - [`planetiler-openmaptiles`](https://github.com/openmaptiles/planetiler-openmaptiles) submodule (BSD 3-Clause License) - Schema - The cartography and visual design features of the map tile schema are licensed diff --git a/planetiler-benchmarks/pom.xml b/planetiler-benchmarks/pom.xml index 2cf1c56a0e..07839de7f9 100644 --- a/planetiler-benchmarks/pom.xml +++ b/planetiler-benchmarks/pom.xml @@ -23,6 +23,11 @@ planetiler-openmaptiles ${revision} + + com.onthegomap.planetiler + planetiler-experimental + ${revision} + diff --git a/planetiler-benchmarks/src/main/java/com/onthegomap/planetiler/benchmarks/BenchmarkLua.java b/planetiler-benchmarks/src/main/java/com/onthegomap/planetiler/benchmarks/BenchmarkLua.java new file mode 100644 index 0000000000..0cf4d51b50 --- /dev/null +++ b/planetiler-benchmarks/src/main/java/com/onthegomap/planetiler/benchmarks/BenchmarkLua.java @@ -0,0 +1,40 @@ +package com.onthegomap.planetiler.benchmarks; + +import com.onthegomap.planetiler.FeatureCollector; +import com.onthegomap.planetiler.config.Arguments; +import com.onthegomap.planetiler.config.PlanetilerConfig; +import com.onthegomap.planetiler.experimental.lua.LuaEnvironment; +import com.onthegomap.planetiler.geo.GeoUtils; +import com.onthegomap.planetiler.reader.SimpleFeature; +import com.onthegomap.planetiler.stats.Stats; +import com.onthegomap.planetiler.util.Format; +import java.io.IOException; +import java.nio.file.Path; +import java.util.List; +import java.util.Map; +import org.locationtech.jts.geom.CoordinateXY; + +public class BenchmarkLua { + + public static void main(String[] args) throws IOException { + var env = + LuaEnvironment.loadScript(Arguments.of(), Path.of("planetiler-experimental/src/test/resources/power.lua")); + var feature = SimpleFeature.createFakeOsmFeature(GeoUtils.JTS_FACTORY.createPoint(new CoordinateXY(0, 0)), Map.of(), + "", "", 1, List.of()); + int batch = 1_000_000; + var fc = new FeatureCollector.Factory(PlanetilerConfig.defaults(), Stats.inMemory()); + for (int i = 0; i < batch; i++) { + env.profile.processFeature(feature, fc.get(feature)); + } + long start = System.currentTimeMillis(); + int num = 0; + do { + for (int i = 0; i < batch; i++) { + env.profile.processFeature(feature, fc.get(feature)); + } + num += batch; + } while (System.currentTimeMillis() - start < 1_000); + long end = System.currentTimeMillis(); + System.err.println(Format.defaultInstance().numeric(num / ((end - start) / 1000.0)) + " calls/sec"); + } +} diff --git a/planetiler-core/pom.xml b/planetiler-core/pom.xml index 9505b22fee..44d8178814 100644 --- a/planetiler-core/pom.xml +++ b/planetiler-core/pom.xml @@ -154,6 +154,10 @@ geopackage ${geopackage.version} + + org.snakeyaml + snakeyaml-engine + diff --git a/planetiler-core/src/main/java/com/onthegomap/planetiler/Planetiler.java b/planetiler-core/src/main/java/com/onthegomap/planetiler/Planetiler.java index e23b1926c5..b2bd98a329 100644 --- a/planetiler-core/src/main/java/com/onthegomap/planetiler/Planetiler.java +++ b/planetiler-core/src/main/java/com/onthegomap/planetiler/Planetiler.java @@ -491,6 +491,10 @@ public Planetiler setDefaultLanguages(List languages) { return this; } + public List getDefaultLanguages() { + return languages; + } + /** * Updates {@link #translations()} to use name translations fetched from wikidata based on the * wikidata tag on OSM elements. diff --git a/planetiler-core/src/main/java/com/onthegomap/planetiler/Profile.java b/planetiler-core/src/main/java/com/onthegomap/planetiler/Profile.java index 8bb2977bbe..92aed92868 100644 --- a/planetiler-core/src/main/java/com/onthegomap/planetiler/Profile.java +++ b/planetiler-core/src/main/java/com/onthegomap/planetiler/Profile.java @@ -7,6 +7,7 @@ import com.onthegomap.planetiler.reader.osm.OsmRelationInfo; import com.onthegomap.planetiler.util.Wikidata; import java.util.List; +import java.util.Locale; import java.util.function.Consumer; /** @@ -114,7 +115,9 @@ default List postProcessLayerFeatures(String layer, int zoom * * @see MBTiles specification */ - String name(); + default String name() { + return getClass().getSimpleName().toLowerCase(Locale.ROOT); + } /** * Returns the description of the generated tileset to put into {@link Mbtiles} metadata diff --git a/planetiler-core/src/main/java/com/onthegomap/planetiler/config/Arguments.java b/planetiler-core/src/main/java/com/onthegomap/planetiler/config/Arguments.java index 573ad2f59b..3af0e98736 100644 --- a/planetiler-core/src/main/java/com/onthegomap/planetiler/config/Arguments.java +++ b/planetiler-core/src/main/java/com/onthegomap/planetiler/config/Arguments.java @@ -426,13 +426,17 @@ public int threads() { public Stats getStats() { String prometheus = getArg("pushgateway"); if (prometheus != null && !prometheus.isBlank()) { - LOGGER.info("argument: stats=use prometheus push gateway stats"); + if (!silent) { + LOGGER.info("argument: stats=use prometheus push gateway stats"); + } String job = getString("pushgateway.job", "prometheus pushgateway job ID", "planetiler"); Duration interval = getDuration("pushgateway.interval", "how often to send stats to prometheus push gateway", "15s"); return Stats.prometheusPushGateway(prometheus, job, interval); } else { - LOGGER.info("argument: stats=use in-memory stats"); + if (!silent) { + LOGGER.info("argument: stats=use in-memory stats"); + } return Stats.inMemory(); } } diff --git a/planetiler-custommap/src/main/java/com/onthegomap/planetiler/custommap/YAML.java b/planetiler-core/src/main/java/com/onthegomap/planetiler/util/YAML.java similarity index 97% rename from planetiler-custommap/src/main/java/com/onthegomap/planetiler/custommap/YAML.java rename to planetiler-core/src/main/java/com/onthegomap/planetiler/util/YAML.java index 2b98417be7..780eaff1ed 100644 --- a/planetiler-custommap/src/main/java/com/onthegomap/planetiler/custommap/YAML.java +++ b/planetiler-core/src/main/java/com/onthegomap/planetiler/util/YAML.java @@ -1,4 +1,4 @@ -package com.onthegomap.planetiler.custommap; +package com.onthegomap.planetiler.util; import com.fasterxml.jackson.databind.ObjectMapper; import java.io.ByteArrayInputStream; diff --git a/planetiler-core/src/main/java/com/onthegomap/planetiler/validator/BaseSchemaValidator.java b/planetiler-core/src/main/java/com/onthegomap/planetiler/validator/BaseSchemaValidator.java new file mode 100644 index 0000000000..57fab3aba0 --- /dev/null +++ b/planetiler-core/src/main/java/com/onthegomap/planetiler/validator/BaseSchemaValidator.java @@ -0,0 +1,231 @@ +package com.onthegomap.planetiler.validator; + +import com.onthegomap.planetiler.FeatureCollector; +import com.onthegomap.planetiler.Profile; +import com.onthegomap.planetiler.config.Arguments; +import com.onthegomap.planetiler.config.PlanetilerConfig; +import com.onthegomap.planetiler.geo.GeometryType; +import com.onthegomap.planetiler.reader.SimpleFeature; +import com.onthegomap.planetiler.stats.Stats; +import com.onthegomap.planetiler.util.AnsiColors; +import com.onthegomap.planetiler.util.FileWatcher; +import com.onthegomap.planetiler.util.Format; +import com.onthegomap.planetiler.util.Try; +import java.io.PrintStream; +import java.nio.file.Path; +import java.time.Duration; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.Locale; +import java.util.Objects; +import java.util.Set; +import java.util.TreeSet; +import java.util.concurrent.ConcurrentHashMap; +import org.apache.commons.lang3.exception.ExceptionUtils; +import org.locationtech.jts.geom.Geometry; +import org.locationtech.jts.io.ParseException; +import org.locationtech.jts.io.WKTReader; + +/** + * Verifies that a profile maps input elements map to expected output vector tile features as defined by a + * {@link SchemaSpecification} instance. + */ +public abstract class BaseSchemaValidator { + + private static final String PASS_BADGE = AnsiColors.greenBackground(" PASS "); + private static final String FAIL_BADGE = AnsiColors.redBackground(" FAIL "); + protected final Arguments args; + protected final PrintStream output; + + protected BaseSchemaValidator(Arguments args, PrintStream output) { + this.args = args; + this.output = output; + } + + protected static boolean hasCause(Throwable t, Class cause) { + return t != null && (cause.isInstance(t) || hasCause(t.getCause(), cause)); + } + + protected void runOrWatch() { + var watch = + args.getBoolean("watch", "Watch files for changes and re-run validation when schema or spec changes", false); + + output.println("OK"); + var paths = validateFromCli(); + + if (watch) { + output.println(); + output.println("Watching filesystem for changes..."); + var watcher = FileWatcher.newWatcher(paths.toArray(Path[]::new)); + watcher.pollForChanges(Duration.ofMillis(300), changed -> validateFromCli()); + } + } + + public Set validateFromCli() { + Set pathsToWatch = ConcurrentHashMap.newKeySet(); + output.println(); + output.println("Validating..."); + output.println(); + BaseSchemaValidator.Result result; + result = validate(pathsToWatch); + if (result != null) { + + int failed = 0, passed = 0; + List failures = new ArrayList<>(); + for (var example : result.results) { + if (example.ok()) { + passed++; + output.printf("%s %s%n", PASS_BADGE, example.example().name()); + } else { + failed++; + printFailure(example, output); + failures.add(example); + } + } + if (!failures.isEmpty()) { + output.println(); + output.println("Summary of failures:"); + for (var failure : failures) { + printFailure(failure, output); + } + } + List summary = new ArrayList<>(); + boolean none = (passed + failed) == 0; + if (none || failed > 0) { + summary.add(AnsiColors.redBold(failed + " failed")); + } + if (none || passed > 0) { + summary.add(AnsiColors.greenBold(passed + " passed")); + } + if (none || passed > 0 && failed > 0) { + summary.add((failed + passed) + " total"); + } + output.println(); + output.println(String.join(", ", summary)); + } + return pathsToWatch; + } + + protected abstract Result validate(Set pathsToWatch); + + private static void printFailure(ExampleResult example, PrintStream output) { + output.printf("%s %s%n", FAIL_BADGE, example.example().name()); + if (example.issues.isFailure()) { + output.println(ExceptionUtils.getStackTrace(example.issues.exception()).indent(4).stripTrailing()); + } else { + for (var issue : example.issues().get()) { + output.println(" ● " + issue.indent(4).strip()); + } + } + } + + private static Geometry parseGeometry(String geometry) { + String wkt = switch (geometry.toLowerCase(Locale.ROOT).trim()) { + case "point" -> "POINT (0 0)"; + case "line" -> "LINESTRING (0 0, 1 1)"; + case "polygon" -> "POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0))"; + default -> geometry; + }; + try { + return new WKTReader().read(wkt); + } catch (ParseException e) { + throw new IllegalArgumentException(""" + Bad geometry: "%s", must be "point" "line" "polygon" or a valid WKT string. + """.formatted(geometry)); + } + } + + /** Returns the result of validating {@code profile} against the examples in {@code specification}. */ + public static Result validate(Profile profile, SchemaSpecification specification, PlanetilerConfig config) { + var featureCollectorFactory = new FeatureCollector.Factory(config, Stats.inMemory()); + return new Result(specification.examples().stream().map(example -> new ExampleResult(example, Try.apply(() -> { + List issues = new ArrayList<>(); + var input = example.input(); + var expectedFeatures = example.output(); + var geometry = parseGeometry(input.geometry()); + var feature = SimpleFeature.create(geometry, input.tags(), input.source(), null, 0); + var collector = featureCollectorFactory.get(feature); + profile.processFeature(feature, collector); + List result = new ArrayList<>(); + collector.forEach(result::add); + if (result.size() != expectedFeatures.size()) { + issues.add( + "Different number of elements, expected=%s actual=%s".formatted(expectedFeatures.size(), result.size())); + } else { + // TODO print a diff of the input and output feature YAML representations + for (int i = 0; i < expectedFeatures.size(); i++) { + var expected = expectedFeatures.get(i); + var actual = result.stream().max(proximityTo(expected)).orElseThrow(); + result.remove(actual); + var actualTags = actual.getAttrsAtZoom(expected.atZoom()); + String prefix = "feature[%d]".formatted(i); + validate(prefix + ".layer", issues, expected.layer(), actual.getLayer()); + validate(prefix + ".minzoom", issues, expected.minZoom(), actual.getMinZoom()); + validate(prefix + ".maxzoom", issues, expected.maxZoom(), actual.getMaxZoom()); + validate(prefix + ".minsize", issues, expected.minSize(), actual.getMinPixelSizeAtZoom(expected.atZoom())); + validate(prefix + ".geometry", issues, expected.geometry(), GeometryType.typeOf(actual.getGeometry())); + Set tags = new TreeSet<>(actualTags.keySet()); + expected.tags().forEach((tag, value) -> { + validate(prefix + ".tags[\"%s\"]".formatted(tag), issues, value, actualTags.get(tag), false); + tags.remove(tag); + }); + if (Boolean.FALSE.equals(expected.allowExtraTags())) { + for (var tag : tags) { + validate(prefix + ".tags[\"%s\"]".formatted(tag), issues, null, actualTags.get(tag), false); + } + } + } + } + return issues; + }))).toList()); + } + + private static Comparator proximityTo(SchemaSpecification.OutputFeature expected) { + return Comparator.comparingInt(item -> (Objects.equals(item.getLayer(), expected.layer()) ? 2 : 0) + + (Objects.equals(GeometryType.typeOf(item.getGeometry()), expected.geometry()) ? 1 : 0)); + } + + private static void validate(String field, List issues, T expected, T actual, boolean ignoreWhenNull) { + if ((!ignoreWhenNull || expected != null) && !Objects.equals(expected, actual)) { + // handle when expected and actual are int/long or long/int + if (expected instanceof Number && actual instanceof Number && expected.toString().equals(actual.toString())) { + return; + } + issues.add("%s: expected <%s> actual <%s>".formatted(field, format(expected), format(actual))); + } + } + + private static String format(Object o) { + if (o == null) { + return "null"; + } else if (o instanceof String s) { + return Format.quote(s); + } else { + return o.toString(); + } + } + + private static void validate(String field, List issues, T expected, T actual) { + validate(field, issues, expected, actual, true); + } + + /** Result of comparing the output vector tile feature to what was expected. */ + public record ExampleResult( + SchemaSpecification.Example example, + // TODO include a symmetric diff so we can pretty-print the expected/actual output diff + Try> issues + ) { + + public boolean ok() { + return issues.isSuccess() && issues.get().isEmpty(); + } + } + + public record Result(List results) { + + public boolean ok() { + return results.stream().allMatch(ExampleResult::ok); + } + } +} diff --git a/planetiler-custommap/src/main/java/com/onthegomap/planetiler/custommap/validator/SchemaSpecification.java b/planetiler-core/src/main/java/com/onthegomap/planetiler/validator/SchemaSpecification.java similarity index 88% rename from planetiler-custommap/src/main/java/com/onthegomap/planetiler/custommap/validator/SchemaSpecification.java rename to planetiler-core/src/main/java/com/onthegomap/planetiler/validator/SchemaSpecification.java index 65385adc91..2014e9b9af 100644 --- a/planetiler-custommap/src/main/java/com/onthegomap/planetiler/custommap/validator/SchemaSpecification.java +++ b/planetiler-core/src/main/java/com/onthegomap/planetiler/validator/SchemaSpecification.java @@ -1,17 +1,21 @@ -package com.onthegomap.planetiler.custommap.validator; +package com.onthegomap.planetiler.validator; import static com.onthegomap.planetiler.config.PlanetilerConfig.MAX_MAXZOOM; import com.fasterxml.jackson.annotation.JsonFormat; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; -import com.onthegomap.planetiler.custommap.YAML; import com.onthegomap.planetiler.geo.GeometryType; +import com.onthegomap.planetiler.util.YAML; import java.nio.file.Path; import java.util.List; import java.util.Map; -/** A model of example input source features and expected output vector tile features that a schema should produce. */ +/** + * A model of example input source features and expected output vector tile features that a schema should produce. + *

+ * Executed by a subclass of {@link BaseSchemaValidator}. + */ @JsonIgnoreProperties(ignoreUnknown = true) public record SchemaSpecification(List examples) { diff --git a/planetiler-core/src/test/java/com/onthegomap/planetiler/PlanetilerTests.java b/planetiler-core/src/test/java/com/onthegomap/planetiler/PlanetilerTests.java index d1224780f3..ade78a6639 100644 --- a/planetiler-core/src/test/java/com/onthegomap/planetiler/PlanetilerTests.java +++ b/planetiler-core/src/test/java/com/onthegomap/planetiler/PlanetilerTests.java @@ -204,18 +204,6 @@ private PlanetilerResults runWithReaderFeaturesProfile( ); } - private PlanetilerResults runWithOsmElements( - Map args, - List features, - BiConsumer profileFunction - ) throws Exception { - return run( - args, - (featureGroup, profile, config) -> processOsmFeatures(featureGroup, profile, config, features), - TestProfile.processSourceFeatures(profileFunction) - ); - } - private PlanetilerResults runWithOsmElements( Map args, List features, diff --git a/planetiler-core/src/test/java/com/onthegomap/planetiler/TestUtils.java b/planetiler-core/src/test/java/com/onthegomap/planetiler/TestUtils.java index 5064b1ff65..876ad03ffa 100644 --- a/planetiler-core/src/test/java/com/onthegomap/planetiler/TestUtils.java +++ b/planetiler-core/src/test/java/com/onthegomap/planetiler/TestUtils.java @@ -7,6 +7,7 @@ import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.fail; +import static org.junit.jupiter.api.DynamicTest.dynamicTest; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; @@ -26,6 +27,8 @@ import com.onthegomap.planetiler.mbtiles.Verify; import com.onthegomap.planetiler.reader.SourceFeature; import com.onthegomap.planetiler.stats.Stats; +import com.onthegomap.planetiler.validator.BaseSchemaValidator; +import com.onthegomap.planetiler.validator.SchemaSpecification; import java.io.IOException; import java.io.UncheckedIOException; import java.nio.file.Files; @@ -47,7 +50,9 @@ import java.util.TreeSet; import java.util.function.Function; import java.util.stream.Collectors; +import java.util.stream.Stream; import org.apache.commons.lang3.reflect.FieldUtils; +import org.junit.jupiter.api.DynamicNode; import org.locationtech.jts.algorithm.Orientation; import org.locationtech.jts.geom.Coordinate; import org.locationtech.jts.geom.CoordinateSequence; @@ -340,6 +345,20 @@ public static Path extractPathToResource(Path tempDir, String resource, String l return path; } + public static Stream validateProfile(Profile profile, String spec) { + return validateProfile(profile, SchemaSpecification.load(spec)); + } + + public static Stream validateProfile(Profile profile, SchemaSpecification spec) { + var result = BaseSchemaValidator.validate(profile, spec, PlanetilerConfig.defaults()); + return result.results().stream().map(test -> dynamicTest(test.example().name(), () -> { + var issues = test.issues().get(); + if (!issues.isEmpty()) { + fail("Failed with " + issues.size() + " issues:\n" + String.join("\n", issues)); + } + })); + } + public interface GeometryComparision { Geometry geom(); @@ -632,7 +651,7 @@ public static void assertMinFeatureCount(Mbtiles db, String layer, int zoom, Map try { int num = Verify.getNumFeatures(db, layer, zoom, attrs, envelope, clazz); - assertTrue(expected < num, + assertTrue(expected <= num, "z%d features in %s, expected at least %d got %d".formatted(zoom, layer, expected, num)); } catch (GeometryException e) { fail(e); diff --git a/planetiler-core/src/test/java/com/onthegomap/planetiler/validator/BaseSchemaValidatorTest.java b/planetiler-core/src/test/java/com/onthegomap/planetiler/validator/BaseSchemaValidatorTest.java new file mode 100644 index 0000000000..5ce9bd1ced --- /dev/null +++ b/planetiler-core/src/test/java/com/onthegomap/planetiler/validator/BaseSchemaValidatorTest.java @@ -0,0 +1,204 @@ +package com.onthegomap.planetiler.validator; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import com.onthegomap.planetiler.FeatureCollector; +import com.onthegomap.planetiler.Profile; +import com.onthegomap.planetiler.TestUtils; +import com.onthegomap.planetiler.config.Arguments; +import com.onthegomap.planetiler.config.PlanetilerConfig; +import com.onthegomap.planetiler.reader.SourceFeature; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.PrintStream; +import java.io.UncheckedIOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Path; +import java.util.Set; +import java.util.stream.Stream; +import org.junit.jupiter.api.DynamicNode; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestFactory; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; + +class BaseSchemaValidatorTest { + + private final String goodSpecString = """ + examples: + - name: test output + input: + source: osm + geometry: polygon + tags: + natural: water + output: + - layer: water + geometry: polygon + tags: + natural: water + """; + private final SchemaSpecification goodSpec = SchemaSpecification.load(goodSpecString); + + private final Profile waterSchema = new Profile() { + @Override + public void processFeature(SourceFeature sourceFeature, FeatureCollector features) { + if (sourceFeature.canBePolygon() && sourceFeature.hasTag("natural", "water")) { + features.polygon("water") + .setMinPixelSize(10) + .inheritAttrFromSource("natural"); + } + } + + @Override + public String name() { + return "test profile"; + } + }; + + private Result validate(Profile profile, String spec) { + var result = BaseSchemaValidator.validate( + profile, + SchemaSpecification.load(spec), + PlanetilerConfig.defaults() + ); + for (var example : result.results()) { + if (example.issues().isFailure()) { + assertNotNull(example.issues().get()); + } + } + // also exercise the cli writer and return what it would have printed to stdout + var cliOutput = validateCli(profile, SchemaSpecification.load(spec)); + return new Result(result, cliOutput); + } + + private String validateCli(Profile profile, SchemaSpecification spec) { + try ( + var baos = new ByteArrayOutputStream(); + var printStream = new PrintStream(baos, true, StandardCharsets.UTF_8) + ) { + new BaseSchemaValidator(Arguments.of(), printStream) { + @Override + protected Result validate(Set pathsToWatch) { + return validate(profile, spec, PlanetilerConfig.from(args)); + } + }.validateFromCli(); + return baos.toString(StandardCharsets.UTF_8); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + + private Result validateWater(String layer, String geometry, String tags, String allowExtraTags) throws IOException { + return validate( + waterSchema, + """ + examples: + - name: test output + input: + source: osm + geometry: polygon + tags: + natural: water + output: + layer: %s + geometry: %s + %s + tags: + %s + """.formatted(layer, geometry, allowExtraTags == null ? "" : allowExtraTags, + tags == null ? "" : tags.indent(6).strip()) + ); + } + + @ParameterizedTest + @CsvSource(value = { + "true,water,polygon,natural: water,", + "true,water,polygon,,", + "true,water,polygon,'natural: water\nother: null',", + "false,water,polygon,natural: null,", + "false,water2,polygon,natural: water,", + "false,water,line,natural: water,", + "false,water,line,natural: water,", + "false,water,polygon,natural: water2,", + "false,water,polygon,'natural: water\nother: value',", + + "true,water,polygon,natural: water,allow_extra_tags: true", + "true,water,polygon,natural: water,allow_extra_tags: false", + "true,water,polygon,,allow_extra_tags: true", + "false,water,polygon,,allow_extra_tags: false", + + "true,water,polygon,,min_size: 10", + "false,water,polygon,,min_size: 9", + }) + void testValidateWaterPolygon(boolean shouldBeOk, String layer, String geometry, String tags, String allowExtraTags) + throws IOException { + var results = validateWater(layer, geometry, tags, allowExtraTags); + assertEquals(1, results.output.results().size()); + assertEquals("test output", results.output.results().get(0).example().name()); + if (shouldBeOk) { + assertTrue(results.output.ok(), results.toString()); + assertFalse(results.cliOutput.contains("FAIL"), "contained FAIL but should not have: " + results.cliOutput); + } else { + assertFalse(results.output.ok(), "Expected an issue, but there were none"); + assertTrue(results.cliOutput.contains("FAIL"), "did not contain FAIL but should have: " + results.cliOutput); + } + } + + @Test + void testValidationFailsWrongNumberOfFeatures() throws IOException { + var results = validate( + waterSchema, + """ + examples: + - name: test output + input: + source: osm + geometry: polygon + tags: + natural: water + output: + """ + ); + assertFalse(results.output.ok(), results.toString()); + + results = validate( + waterSchema, + """ + examples: + - name: test output + input: + source: osm + geometry: polygon + tags: + natural: water + output: + - layer: water + geometry: polygon + tags: + natural: water + - layer: water2 + geometry: polygon + tags: + natural: water2 + """ + ); + assertFalse(results.output.ok(), results.toString()); + } + + @TestFactory + Stream testJunitAdapterSpec() { + return TestUtils.validateProfile(waterSchema, goodSpec); + } + + @TestFactory + Stream testJunitAdapterString() { + return TestUtils.validateProfile(waterSchema, goodSpecString); + } + + + record Result(BaseSchemaValidator.Result output, String cliOutput) {} +} diff --git a/planetiler-custommap/pom.xml b/planetiler-custommap/pom.xml index 40548cc7e2..c02e323e21 100644 --- a/planetiler-custommap/pom.xml +++ b/planetiler-custommap/pom.xml @@ -18,10 +18,6 @@ planetiler-core ${project.parent.version} - - org.snakeyaml - snakeyaml-engine - org.commonmark commonmark diff --git a/planetiler-custommap/src/main/java/com/onthegomap/planetiler/custommap/ConfiguredMapMain.java b/planetiler-custommap/src/main/java/com/onthegomap/planetiler/custommap/ConfiguredMapMain.java index 23283e531d..ff73a1f56c 100644 --- a/planetiler-custommap/src/main/java/com/onthegomap/planetiler/custommap/ConfiguredMapMain.java +++ b/planetiler-custommap/src/main/java/com/onthegomap/planetiler/custommap/ConfiguredMapMain.java @@ -5,6 +5,7 @@ import com.onthegomap.planetiler.custommap.configschema.DataSourceType; import com.onthegomap.planetiler.custommap.configschema.SchemaConfig; import com.onthegomap.planetiler.custommap.expression.ParseException; +import com.onthegomap.planetiler.util.YAML; import java.nio.file.Files; import java.nio.file.Path; diff --git a/planetiler-custommap/src/main/java/com/onthegomap/planetiler/custommap/configschema/SchemaConfig.java b/planetiler-custommap/src/main/java/com/onthegomap/planetiler/custommap/configschema/SchemaConfig.java index c4b70912a9..fc11d488cc 100644 --- a/planetiler-custommap/src/main/java/com/onthegomap/planetiler/custommap/configschema/SchemaConfig.java +++ b/planetiler-custommap/src/main/java/com/onthegomap/planetiler/custommap/configschema/SchemaConfig.java @@ -1,7 +1,7 @@ package com.onthegomap.planetiler.custommap.configschema; import com.fasterxml.jackson.annotation.JsonProperty; -import com.onthegomap.planetiler.custommap.YAML; +import com.onthegomap.planetiler.util.YAML; import java.nio.file.Path; import java.util.Collection; import java.util.Map; diff --git a/planetiler-custommap/src/main/java/com/onthegomap/planetiler/custommap/validator/SchemaValidator.java b/planetiler-custommap/src/main/java/com/onthegomap/planetiler/custommap/validator/SchemaValidator.java index b112a189fb..53b9f64d1d 100644 --- a/planetiler-custommap/src/main/java/com/onthegomap/planetiler/custommap/validator/SchemaValidator.java +++ b/planetiler-custommap/src/main/java/com/onthegomap/planetiler/custommap/validator/SchemaValidator.java @@ -1,44 +1,31 @@ package com.onthegomap.planetiler.custommap.validator; import com.fasterxml.jackson.core.JacksonException; -import com.onthegomap.planetiler.FeatureCollector; -import com.onthegomap.planetiler.Profile; import com.onthegomap.planetiler.config.Arguments; -import com.onthegomap.planetiler.config.PlanetilerConfig; import com.onthegomap.planetiler.custommap.ConfiguredProfile; import com.onthegomap.planetiler.custommap.Contexts; -import com.onthegomap.planetiler.custommap.YAML; import com.onthegomap.planetiler.custommap.configschema.SchemaConfig; -import com.onthegomap.planetiler.geo.GeometryType; -import com.onthegomap.planetiler.reader.SimpleFeature; -import com.onthegomap.planetiler.stats.Stats; import com.onthegomap.planetiler.util.AnsiColors; -import com.onthegomap.planetiler.util.FileWatcher; -import com.onthegomap.planetiler.util.Format; -import com.onthegomap.planetiler.util.Try; +import com.onthegomap.planetiler.util.YAML; +import com.onthegomap.planetiler.validator.BaseSchemaValidator; +import com.onthegomap.planetiler.validator.SchemaSpecification; import java.io.PrintStream; import java.nio.file.Path; -import java.time.Duration; -import java.util.ArrayList; -import java.util.Comparator; -import java.util.HashSet; import java.util.List; -import java.util.Locale; -import java.util.Objects; import java.util.Set; -import java.util.TreeSet; import java.util.stream.Stream; import org.apache.commons.lang3.exception.ExceptionUtils; -import org.locationtech.jts.geom.Geometry; -import org.locationtech.jts.io.ParseException; -import org.locationtech.jts.io.WKTReader; import org.snakeyaml.engine.v2.exceptions.YamlEngineException; -/** Verifies that a profile maps input elements map to expected output vector tile features. */ -public class SchemaValidator { +public class SchemaValidator extends BaseSchemaValidator { - private static final String PASS_BADGE = AnsiColors.greenBackground(" PASS "); - private static final String FAIL_BADGE = AnsiColors.redBackground(" FAIL "); + private final Path schemaPath; + + SchemaValidator(Arguments args, String schemaFile, PrintStream output) { + super(args, output); + schemaPath = schemaFile == null ? args.inputFile("schema", "Schema file") : + args.inputFile("schema", "Schema file", Path.of(schemaFile)); + } public static void main(String[] args) { // let users run `verify schema.yml` as a shortcut @@ -48,36 +35,24 @@ public static void main(String[] args) { args = Stream.of(args).skip(1).toArray(String[]::new); } var arguments = Arguments.fromEnvOrArgs(args); - var schema = schemaFile == null ? arguments.inputFile("schema", "Schema file") : - arguments.inputFile("schema", "Schema file", Path.of(schemaFile)); - var watch = - arguments.getBoolean("watch", "Watch files for changes and re-run validation when schema or spec changes", false); - - - PrintStream output = System.out; - output.println("OK"); - var paths = validateFromCli(schema, output); - - if (watch) { - output.println(); - output.println("Watching filesystem for changes..."); - var watcher = FileWatcher.newWatcher(paths.toArray(Path[]::new)); - watcher.pollForChanges(Duration.ofMillis(300), changed -> validateFromCli(schema, output)); - } + new SchemaValidator(arguments, schemaFile, System.out).runOrWatch(); } - private static boolean hasCause(Throwable t, Class cause) { - return t != null && (cause.isInstance(t) || hasCause(t.getCause(), cause)); + + /** + * Returns the result of validating the profile defined by {@code schema} against the examples in + * {@code specification}. + */ + public static Result validate(SchemaConfig schema, SchemaSpecification specification) { + var context = Contexts.buildRootContext(Arguments.of().silence(), schema.args()); + return validate(new ConfiguredProfile(schema, context), specification, context.config()); } - static Set validateFromCli(Path schemaPath, PrintStream output) { - Set pathsToWatch = new HashSet<>(); - pathsToWatch.add(schemaPath); - output.println(); - output.println("Validating..."); - output.println(); - SchemaValidator.Result result; + @Override + protected Result validate(Set pathsToWatch) { + Result result = null; try { + pathsToWatch.add(schemaPath); var schema = SchemaConfig.load(schemaPath); var examples = schema.examples(); // examples can either be embedded in the yaml file, or referenced @@ -108,169 +83,7 @@ static Set validateFromCli(Path schemaPath, PrintStream output) { String.join("\n", ExceptionUtils.getStackTrace(rootCause))) .indent(4)); } - return pathsToWatch; - } - int failed = 0, passed = 0; - List failures = new ArrayList<>(); - for (var example : result.results) { - if (example.ok()) { - passed++; - output.printf("%s %s%n", PASS_BADGE, example.example().name()); - } else { - failed++; - printFailure(example, output); - failures.add(example); - } - } - if (!failures.isEmpty()) { - output.println(); - output.println("Summary of failures:"); - for (var failure : failures) { - printFailure(failure, output); - } - } - List summary = new ArrayList<>(); - boolean none = (passed + failed) == 0; - if (none || failed > 0) { - summary.add(AnsiColors.redBold(failed + " failed")); - } - if (none || passed > 0) { - summary.add(AnsiColors.greenBold(passed + " passed")); - } - if (none || passed > 0 && failed > 0) { - summary.add((failed + passed) + " total"); - } - output.println(); - output.println(String.join(", ", summary)); - return pathsToWatch; - } - - private static void printFailure(ExampleResult example, PrintStream output) { - output.printf("%s %s%n", FAIL_BADGE, example.example().name()); - if (example.issues.isFailure()) { - output.println(ExceptionUtils.getStackTrace(example.issues.exception()).indent(4).stripTrailing()); - } else { - for (var issue : example.issues().get()) { - output.println(" ● " + issue.indent(4).strip()); - } - } - } - - private static Geometry parseGeometry(String geometry) { - String wkt = switch (geometry.toLowerCase(Locale.ROOT).trim()) { - case "point" -> "POINT (0 0)"; - case "line" -> "LINESTRING (0 0, 1 1)"; - case "polygon" -> "POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0))"; - default -> geometry; - }; - try { - return new WKTReader().read(wkt); - } catch (ParseException e) { - throw new IllegalArgumentException(""" - Bad geometry: "%s", must be "point" "line" "polygon" or a valid WKT string. - """.formatted(geometry)); - } - } - - /** - * Returns the result of validating the profile defined by {@code schema} against the examples in - * {@code specification}. - */ - public static Result validate(SchemaConfig schema, SchemaSpecification specification) { - var context = Contexts.buildRootContext(Arguments.of().silence(), schema.args()); - return validate(new ConfiguredProfile(schema, context), specification, context.config()); - } - - /** Returns the result of validating {@code profile} against the examples in {@code specification}. */ - public static Result validate(Profile profile, SchemaSpecification specification, PlanetilerConfig config) { - var featureCollectorFactory = new FeatureCollector.Factory(config, Stats.inMemory()); - return new Result(specification.examples().stream().map(example -> new ExampleResult(example, Try.apply(() -> { - List issues = new ArrayList<>(); - var input = example.input(); - var expectedFeatures = example.output(); - var geometry = parseGeometry(input.geometry()); - var feature = SimpleFeature.create(geometry, input.tags(), input.source(), null, 0); - var collector = featureCollectorFactory.get(feature); - profile.processFeature(feature, collector); - List result = new ArrayList<>(); - collector.forEach(result::add); - if (result.size() != expectedFeatures.size()) { - issues.add( - "Different number of elements, expected=%s actual=%s".formatted(expectedFeatures.size(), result.size())); - } else { - // TODO print a diff of the input and output feature YAML representations - for (int i = 0; i < expectedFeatures.size(); i++) { - var expected = expectedFeatures.get(i); - var actual = result.stream().max(proximityTo(expected)).orElseThrow(); - result.remove(actual); - var actualTags = actual.getAttrsAtZoom(expected.atZoom()); - String prefix = "feature[%d]".formatted(i); - validate(prefix + ".layer", issues, expected.layer(), actual.getLayer()); - validate(prefix + ".minzoom", issues, expected.minZoom(), actual.getMinZoom()); - validate(prefix + ".maxzoom", issues, expected.maxZoom(), actual.getMaxZoom()); - validate(prefix + ".minsize", issues, expected.minSize(), actual.getMinPixelSizeAtZoom(expected.atZoom())); - validate(prefix + ".geometry", issues, expected.geometry(), GeometryType.typeOf(actual.getGeometry())); - Set tags = new TreeSet<>(actualTags.keySet()); - expected.tags().forEach((tag, value) -> { - validate(prefix + ".tags[\"%s\"]".formatted(tag), issues, value, actualTags.get(tag), false); - tags.remove(tag); - }); - if (Boolean.FALSE.equals(expected.allowExtraTags())) { - for (var tag : tags) { - validate(prefix + ".tags[\"%s\"]".formatted(tag), issues, null, actualTags.get(tag), false); - } - } - } - } - return issues; - }))).toList()); - } - - private static Comparator proximityTo(SchemaSpecification.OutputFeature expected) { - return Comparator.comparingInt(item -> (Objects.equals(item.getLayer(), expected.layer()) ? 2 : 0) + - (Objects.equals(GeometryType.typeOf(item.getGeometry()), expected.geometry()) ? 1 : 0)); - } - - private static void validate(String field, List issues, T expected, T actual, boolean ignoreWhenNull) { - if ((!ignoreWhenNull || expected != null) && !Objects.equals(expected, actual)) { - // handle when expected and actual are int/long or long/int - if (expected instanceof Number && actual instanceof Number && expected.toString().equals(actual.toString())) { - return; - } - issues.add("%s: expected <%s> actual <%s>".formatted(field, format(expected), format(actual))); - } - } - - private static String format(Object o) { - if (o == null) { - return "null"; - } else if (o instanceof String s) { - return Format.quote(s); - } else { - return o.toString(); - } - } - - private static void validate(String field, List issues, T expected, T actual) { - validate(field, issues, expected, actual, true); - } - - /** Result of comparing the output vector tile feature to what was expected. */ - public record ExampleResult( - SchemaSpecification.Example example, - // TODO include a symmetric diff so we can pretty-print the expected/actual output diff - Try> issues - ) { - - public boolean ok() { - return issues.isSuccess() && issues.get().isEmpty(); - } - } - - public record Result(List results) { - - public boolean ok() { - return results.stream().allMatch(ExampleResult::ok); } + return result; } } diff --git a/planetiler-custommap/src/test/java/com/onthegomap/planetiler/custommap/BooleanExpressionParserTest.java b/planetiler-custommap/src/test/java/com/onthegomap/planetiler/custommap/BooleanExpressionParserTest.java index b7dec4a8d8..5b17b81b59 100644 --- a/planetiler-custommap/src/test/java/com/onthegomap/planetiler/custommap/BooleanExpressionParserTest.java +++ b/planetiler-custommap/src/test/java/com/onthegomap/planetiler/custommap/BooleanExpressionParserTest.java @@ -4,6 +4,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import com.onthegomap.planetiler.expression.Expression; +import com.onthegomap.planetiler.util.YAML; import java.util.Map; import org.junit.jupiter.api.Test; diff --git a/planetiler-custommap/src/test/java/com/onthegomap/planetiler/custommap/ConfigExpressionParserTest.java b/planetiler-custommap/src/test/java/com/onthegomap/planetiler/custommap/ConfigExpressionParserTest.java index 78f8804e25..46c25652ea 100644 --- a/planetiler-custommap/src/test/java/com/onthegomap/planetiler/custommap/ConfigExpressionParserTest.java +++ b/planetiler-custommap/src/test/java/com/onthegomap/planetiler/custommap/ConfigExpressionParserTest.java @@ -8,6 +8,7 @@ import com.onthegomap.planetiler.expression.DataType; import com.onthegomap.planetiler.expression.Expression; import com.onthegomap.planetiler.expression.MultiExpression; +import com.onthegomap.planetiler.util.YAML; import java.util.List; import java.util.Map; import org.junit.jupiter.api.Test; diff --git a/planetiler-custommap/src/test/java/com/onthegomap/planetiler/custommap/SchemaTests.java b/planetiler-custommap/src/test/java/com/onthegomap/planetiler/custommap/SchemaTests.java index 3400fe668d..ab9c918366 100644 --- a/planetiler-custommap/src/test/java/com/onthegomap/planetiler/custommap/SchemaTests.java +++ b/planetiler-custommap/src/test/java/com/onthegomap/planetiler/custommap/SchemaTests.java @@ -1,35 +1,26 @@ package com.onthegomap.planetiler.custommap; -import static org.junit.jupiter.api.DynamicTest.dynamicTest; - +import com.onthegomap.planetiler.TestUtils; +import com.onthegomap.planetiler.config.Arguments; import com.onthegomap.planetiler.custommap.configschema.SchemaConfig; -import com.onthegomap.planetiler.custommap.validator.SchemaSpecification; -import com.onthegomap.planetiler.custommap.validator.SchemaValidator; +import com.onthegomap.planetiler.validator.SchemaSpecification; import java.nio.file.Path; -import java.util.List; -import org.junit.jupiter.api.DynamicTest; +import java.util.stream.Stream; +import org.junit.jupiter.api.DynamicNode; import org.junit.jupiter.api.TestFactory; class SchemaTests { @TestFactory - List shortbread() { - return testSchema("shortbread.yml", "shortbread.spec.yml"); + Stream shortbread() { + return test("shortbread.yml", "shortbread.spec.yml"); } - private List testSchema(String schema, String spec) { + private static Stream test(String schemaFile, String specFile) { var base = Path.of("src", "main", "resources", "samples"); - var result = SchemaValidator.validate( - SchemaConfig.load(base.resolve(schema)), - SchemaSpecification.load(base.resolve(spec)) - ); - return result.results().stream() - .map(test -> dynamicTest(test.example().name(), () -> { - if (test.issues().isFailure()) { - throw test.issues().exception(); - } - if (!test.issues().get().isEmpty()) { - throw new AssertionError("Validation failed:\n" + String.join("\n", test.issues().get())); - } - })).toList(); + SchemaConfig schema = SchemaConfig.load(base.resolve(schemaFile)); + SchemaSpecification specification = SchemaSpecification.load(base.resolve(specFile)); + var context = Contexts.buildRootContext(Arguments.of().silence(), schema.args()); + var profile = new ConfiguredProfile(schema, context); + return TestUtils.validateProfile(profile, specification); } } diff --git a/planetiler-custommap/src/test/java/com/onthegomap/planetiler/custommap/validator/SchemaValidatorTest.java b/planetiler-custommap/src/test/java/com/onthegomap/planetiler/custommap/validator/SchemaValidatorTest.java index eff36cc9c9..3672493398 100644 --- a/planetiler-custommap/src/test/java/com/onthegomap/planetiler/custommap/validator/SchemaValidatorTest.java +++ b/planetiler-custommap/src/test/java/com/onthegomap/planetiler/custommap/validator/SchemaValidatorTest.java @@ -5,7 +5,10 @@ import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertTrue; +import com.onthegomap.planetiler.config.Arguments; import com.onthegomap.planetiler.custommap.configschema.SchemaConfig; +import com.onthegomap.planetiler.validator.BaseSchemaValidator; +import com.onthegomap.planetiler.validator.SchemaSpecification; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.PrintStream; @@ -22,7 +25,7 @@ class SchemaValidatorTest { @TempDir Path tmpDir; - record Result(SchemaValidator.Result output, String cliOutput) {} + record Result(BaseSchemaValidator.Result output, String cliOutput) {} private Result validate(String schema, String spec) throws IOException { var result = SchemaValidator.validate( @@ -57,55 +60,13 @@ private String validateCli(Path path) { var baos = new ByteArrayOutputStream(); var printStream = new PrintStream(baos, true, StandardCharsets.UTF_8) ) { - SchemaValidator.validateFromCli( - path, - printStream - ); + new SchemaValidator(Arguments.of(), path.toString(), printStream).validateFromCli(); return baos.toString(StandardCharsets.UTF_8); } catch (IOException e) { throw new UncheckedIOException(e); } } - String waterSchema = """ - sources: - osm: - type: osm - url: geofabrik:rhode-island - layers: - - id: water - features: - - source: osm - geometry: polygon - min_size: 10 - include_when: - natural: water - attributes: - - key: natural - """; - - private Result validateWater(String layer, String geometry, String tags, String allowExtraTags) throws IOException { - return validate( - waterSchema, - """ - examples: - - name: test output - input: - source: osm - geometry: polygon - tags: - natural: water - output: - layer: %s - geometry: %s - %s - tags: - %s - """.formatted(layer, geometry, allowExtraTags == null ? "" : allowExtraTags, - tags == null ? "" : tags.indent(6).strip()) - ); - } - @ParameterizedTest @CsvSource(value = { "true,water,polygon,natural: water,", @@ -128,37 +89,23 @@ private Result validateWater(String layer, String geometry, String tags, String }) void testValidateWaterPolygon(boolean shouldBeOk, String layer, String geometry, String tags, String allowExtraTags) throws IOException { - var results = validateWater(layer, geometry, tags, allowExtraTags); - assertEquals(1, results.output.results().size()); - assertEquals("test output", results.output.results().get(0).example().name()); - if (shouldBeOk) { - assertTrue(results.output.ok(), results.toString()); - assertFalse(results.cliOutput.contains("FAIL"), "contained FAIL but should not have: " + results.cliOutput); - } else { - assertFalse(results.output.ok(), "Expected an issue, but there were none"); - assertTrue(results.cliOutput.contains("FAIL"), "did not contain FAIL but should have: " + results.cliOutput); - } - } - - @Test - void testValidationFailsWrongNumberOfFeatures() throws IOException { var results = validate( - waterSchema, """ - examples: - - name: test output - input: - source: osm + sources: + osm: + type: osm + url: geofabrik:rhode-island + layers: + - id: water + features: + - source: osm geometry: polygon - tags: + min_size: 10 + include_when: natural: water - output: - """ - ); - assertFalse(results.output.ok(), results.toString()); - - results = validate( - waterSchema, + attributes: + - key: natural + """, """ examples: - name: test output @@ -168,17 +115,23 @@ void testValidationFailsWrongNumberOfFeatures() throws IOException { tags: natural: water output: - - layer: water - geometry: polygon - tags: - natural: water - - layer: water2 - geometry: polygon + layer: %s + geometry: %s + %s tags: - natural: water2 - """ + %s + """.formatted(layer, geometry, allowExtraTags == null ? "" : allowExtraTags, + tags == null ? "" : tags.indent(6).strip()) ); - assertFalse(results.output.ok(), results.toString()); + assertEquals(1, results.output.results().size()); + assertEquals("test output", results.output.results().get(0).example().name()); + if (shouldBeOk) { + assertTrue(results.output.ok(), results.toString()); + assertFalse(results.cliOutput.contains("FAIL"), "contained FAIL but should not have: " + results.cliOutput); + } else { + assertFalse(results.output.ok(), "Expected an issue, but there were none"); + assertTrue(results.cliOutput.contains("FAIL"), "did not contain FAIL but should have: " + results.cliOutput); + } } @Test diff --git a/planetiler-dist/pom.xml b/planetiler-dist/pom.xml index 7af1b23c7a..86f3092ce1 100644 --- a/planetiler-dist/pom.xml +++ b/planetiler-dist/pom.xml @@ -43,6 +43,11 @@ planetiler-custommap ${project.parent.version} + + com.onthegomap.planetiler + planetiler-experimental + ${project.parent.version} + com.onthegomap.planetiler planetiler-examples diff --git a/planetiler-dist/src/main/java/com/onthegomap/planetiler/Main.java b/planetiler-dist/src/main/java/com/onthegomap/planetiler/Main.java index df949ed305..3ce174b325 100644 --- a/planetiler-dist/src/main/java/com/onthegomap/planetiler/Main.java +++ b/planetiler-dist/src/main/java/com/onthegomap/planetiler/Main.java @@ -10,6 +10,9 @@ import com.onthegomap.planetiler.examples.OsmQaTiles; import com.onthegomap.planetiler.examples.ToiletsOverlay; import com.onthegomap.planetiler.examples.ToiletsOverlayLowLevelApi; +import com.onthegomap.planetiler.experimental.lua.GenerateLuaTypes; +import com.onthegomap.planetiler.experimental.lua.LuaMain; +import com.onthegomap.planetiler.experimental.lua.LuaValidator; import com.onthegomap.planetiler.mbtiles.Verify; import com.onthegomap.planetiler.util.TileSizeStats; import com.onthegomap.planetiler.util.TopOsmTiles; @@ -43,12 +46,20 @@ public class Main { entry("generate-custom", ConfiguredMapMain::main), entry("custom", ConfiguredMapMain::main), + entry("lua", LuaMain::main), + entry("lua-types", GenerateLuaTypes::main), + entry("generate-shortbread", bundledSchema("shortbread.yml")), entry("shortbread", bundledSchema("shortbread.yml")), - entry("verify", SchemaValidator::main), + entry("verify", validate()), entry("verify-custom", SchemaValidator::main), entry("verify-schema", SchemaValidator::main), + entry("verify-lua", LuaValidator::main), + entry("validate", validate()), + entry("validate-custom", SchemaValidator::main), + entry("validate-schema", SchemaValidator::main), + entry("validate-lua", LuaValidator::main), entry("example-bikeroutes", BikeRouteOverlay::main), entry("example-toilets", ToiletsOverlay::main), @@ -73,6 +84,16 @@ private static EntryPoint bundledSchema(String path) { ).toArray(String[]::new)); } + private static EntryPoint validate() { + return args -> { + if (Arrays.stream(args).anyMatch(d -> d.endsWith(".lua"))) { + LuaValidator.main(args); + } else { + SchemaValidator.main(args); + } + }; + } + public static void main(String[] args) throws Exception { EntryPoint task = DEFAULT_TASK; @@ -81,6 +102,9 @@ public static void main(String[] args) throws Exception { if (maybeTask.matches("^.*\\.ya?ml$")) { task = ConfiguredMapMain::main; args[0] = "--schema=" + args[0]; + } else if (maybeTask.matches("^.*\\.lua$")) { + task = LuaMain::main; + args[0] = "--script=" + args[0]; } else { EntryPoint taskFromArg0 = ENTRY_POINTS.get(maybeTask); if (taskFromArg0 != null) { diff --git a/planetiler-experimental/pom.xml b/planetiler-experimental/pom.xml new file mode 100644 index 0000000000..b11a29e351 --- /dev/null +++ b/planetiler-experimental/pom.xml @@ -0,0 +1,43 @@ + + + + 4.0.0 + + planetiler-experimental + + + com.onthegomap.planetiler + planetiler-parent + ${revision} + + + + + com.onthegomap.planetiler + planetiler-core + ${project.parent.version} + + + + org.luaj + luaj-jse + 3.0.1 + + + org.apache.bcel + bcel + 6.7.0 + + + + + com.onthegomap.planetiler + planetiler-core + ${project.parent.version} + test-jar + test + + + diff --git a/planetiler-experimental/src/main/java/com/onthegomap/planetiler/experimental/lua/GenerateLuaTypes.java b/planetiler-experimental/src/main/java/com/onthegomap/planetiler/experimental/lua/GenerateLuaTypes.java new file mode 100644 index 0000000000..b2444b27a7 --- /dev/null +++ b/planetiler-experimental/src/main/java/com/onthegomap/planetiler/experimental/lua/GenerateLuaTypes.java @@ -0,0 +1,499 @@ +package com.onthegomap.planetiler.experimental.lua; + +import static com.onthegomap.planetiler.experimental.lua.JavaToLuaCase.transformMemberName; +import static java.util.Map.entry; + +import com.google.common.base.CaseFormat; +import com.google.common.reflect.Invokable; +import com.google.common.reflect.TypeToken; +import com.onthegomap.planetiler.util.Format; +import java.lang.reflect.Executable; +import java.lang.reflect.Field; +import java.lang.reflect.Member; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.lang.reflect.RecordComponent; +import java.lang.reflect.Type; +import java.lang.reflect.TypeVariable; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Comparator; +import java.util.Deque; +import java.util.HashSet; +import java.util.LinkedHashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.TreeMap; +import java.util.TreeSet; +import java.util.stream.Collectors; +import org.luaj.vm2.LuaDouble; +import org.luaj.vm2.LuaInteger; +import org.luaj.vm2.LuaNumber; +import org.luaj.vm2.LuaString; +import org.luaj.vm2.LuaTable; +import org.luaj.vm2.LuaValue; +import org.luaj.vm2.lib.jse.LuaBindMethods; +import org.luaj.vm2.lib.jse.LuaFunctionType; +import org.luaj.vm2.lib.jse.LuaGetter; +import org.luaj.vm2.lib.jse.LuaSetter; +import org.luaj.vm2.lib.jse.LuaType; + +/** + * Generates a lua file with type definitions for the lua environment exposed by planetiler. + * + *

+ * java -jar planetiler.jar lua-types > types.lua
+ * 
+ * + * @see Lua Language Server type annotations + */ +public class GenerateLuaTypes { + private static final Map, String> TYPE_NAMES = Map.ofEntries( + entry(Object.class, "any"), + entry(LuaInteger.class, "integer"), + entry(LuaDouble.class, "number"), + entry(LuaNumber.class, "number"), + entry(LuaString.class, "string"), + entry(LuaTable.class, "table"), + entry(Class.class, "userdata"), + entry(String.class, "string"), + entry(Number.class, "number"), + entry(byte[].class, "string"), + entry(Integer.class, "integer"), + entry(int.class, "integer"), + entry(Long.class, "integer"), + entry(long.class, "integer"), + entry(Short.class, "integer"), + entry(short.class, "integer"), + entry(Byte.class, "integer"), + entry(byte.class, "integer"), + entry(Double.class, "number"), + entry(double.class, "number"), + entry(Float.class, "number"), + entry(float.class, "number"), + entry(boolean.class, "boolean"), + entry(Boolean.class, "boolean"), + entry(Void.class, "nil"), + entry(void.class, "nil") + ); + private static final TypeToken> LIST_TYPE = new TypeToken<>() {}; + private static final TypeToken> MAP_TYPE = new TypeToken<>() {}; + private final Deque debugStack = new LinkedList<>(); + private final Set handled = new HashSet<>(); + private final StringBuilder builder = new StringBuilder(); + private static final String NEWLINE = System.lineSeparator(); + + GenerateLuaTypes() { + write(""" + ---@meta + local types = {} + """); + } + + public static void main(String[] args) { + var generator = new GenerateLuaTypes().generatePlanetiler(); + System.out.println(generator.toString().replaceAll("[\r\n]+", NEWLINE)); + } + + private static String luaClassName(Class clazz) { + return clazz.getName().replaceAll("[\\$\\.]", "_"); + } + + private static boolean differentFromParents2(Invokable invokable, TypeToken superType) { + if (!invokable.getReturnType().equals(superType.resolveType(invokable.getReturnType().getType()))) { + return true; + } + var orig = + invokable.getParameters().stream() + .map(t -> invokable.getOwnerType().resolveType(t.getType().getType())) + .toList(); + var resolved = invokable.getParameters().stream().map(t -> superType.resolveType(t.getType().getType())).toList(); + return !orig.equals(resolved); + } + + private static boolean hasMethod(Class clazz, Method method) { + try { + clazz.getMethod(method.getName(), method.getParameterTypes()); + return true; + } catch (NoSuchMethodException e) { + return false; + } + } + + private void write(String line) { + builder.append(line).append(NEWLINE); + } + + GenerateLuaTypes generatePlanetiler() { + exportGlobalInstance("planetiler", LuaEnvironment.PlanetilerNamespace.class); + for (var clazz : LuaEnvironment.CLASSES_TO_EXPOSE) { + exportGlobalType(clazz); + } + return this; + } + + void exportGlobalInstance(String name, Class clazz) { + write("---@class (exact) " + getInstanceTypeName(clazz)); + write(name + " = {}"); + } + + void exportGlobalType(Class clazz) { + write("---@class (exact) " + getStaticTypeName(clazz)); + write(clazz.getSimpleName() + " = {}"); + } + + private String getStaticTypeName(Class clazz) { + String name = luaClassName(clazz) + "__class"; + debugStack.push(" -> " + clazz.getSimpleName()); + try { + if (handled.add(name)) { + write(getStaticTypeDefinition(clazz)); + } + return name; + } finally { + debugStack.pop(); + } + } + + private String getInstanceTypeName(TypeToken type) { + if (LIST_TYPE.isSupertypeOf(type)) { + return getInstanceTypeName(type.resolveType(LIST_TYPE.getRawType().getTypeParameters()[0])) + "[]"; + } else if (MAP_TYPE.isSupertypeOf(type)) { + return "{[%s]: %s}".formatted( + getInstanceTypeName(type.resolveType(MAP_TYPE.getRawType().getTypeParameters()[0])), + getInstanceTypeName(type.resolveType(MAP_TYPE.getRawType().getTypeParameters()[1])) + ); + } + return getInstanceTypeName(type.getRawType()); + } + + private String getInstanceTypeName(Class clazz) { + if (clazz.getPackageName().startsWith("com.google.protobuf")) { + return "any"; + } + if (LuaValue.class.equals(clazz)) { + throw new IllegalArgumentException("Unhandled LuaValue: " + String.join("", debugStack)); + } + debugStack.push(" -> " + clazz.getSimpleName()); + try { + if (TYPE_NAMES.containsKey(clazz)) { + return TYPE_NAMES.get(clazz); + } + if (clazz.isArray()) { + return getInstanceTypeName(clazz.getComponentType()) + "[]"; + } + if (LuaValue.class.isAssignableFrom(clazz)) { + return "any"; + } + + String name = luaClassName(clazz); + if (handled.add(name)) { + write(getTypeDefinition(clazz)); + } + return name; + } finally { + debugStack.pop(); + } + } + + String getTypeDefinition(Class clazz) { + return generateLuaInstanceTypeDefinition(clazz).generate(); + } + + String getStaticTypeDefinition(Class clazz) { + return generateLuaTypeDefinition(clazz, "__class", true).generate(); + } + + private LuaTypeDefinition generateLuaInstanceTypeDefinition(Class clazz) { + return generateLuaTypeDefinition(clazz, "", false); + } + + private LuaTypeDefinition generateLuaTypeDefinition(Class clazz, String suffix, boolean isStatic) { + TypeToken type = TypeToken.of(clazz); + var definition = new LuaTypeDefinition(type, suffix, isStatic); + + if (!isStatic) { + Type superclass = clazz.getGenericSuperclass(); + if (superclass != null && superclass != Object.class) { + definition.addParent(type.resolveType(superclass)); + } + for (var iface : clazz.getGenericInterfaces()) { + definition.addParent(type.resolveType(iface)); + } + } + + for (var field : clazz.getFields()) { + TypeToken rawType = TypeToken.of(field.getDeclaringClass()).resolveType(field.getGenericType()); + TypeToken typeOnThisClass = type.resolveType(field.getGenericType()); + if (Modifier.isPublic(field.getModifiers()) && isStatic == Modifier.isStatic(field.getModifiers()) && + (isStatic || field.getDeclaringClass() == clazz || !rawType.equals(typeOnThisClass))) { + definition.addField(field); + } + } + + Set recordFields = clazz.isRecord() ? Arrays.stream(clazz.getRecordComponents()) + .map(RecordComponent::getAccessor) + .collect(Collectors.toSet()) : Set.of(); + + // - declare public (static and nonstatic) methods + for (var method : clazz.getMethods()) { + if (Modifier.isPublic(method.getModifiers()) && isStatic == Modifier.isStatic(method.getModifiers())) { + Invokable invokable = type.method(method); + if (!isStatic && !invokable.getOwnerType().equals(type) && !differentFromParents(invokable, type)) { + continue; + } + if (hasMethod(Object.class, method)) { + // skip object methods + } else if (method.isAnnotationPresent(LuaGetter.class) || + (method.getParameterCount() == 0 && recordFields.contains(method))) { + definition.addField(method, method.getGenericReturnType()); + } else if (method.isAnnotationPresent(LuaSetter.class)) { + definition.addField(method, method.getParameterTypes()[0]); + } else { + definition.addMethod(method); + } + } + } + + if (isStatic) { + for (var constructor : clazz.getConstructors()) { + if (Modifier.isPublic(constructor.getModifiers())) { + definition.addMethod("new", constructor, clazz); + } + } + } + return definition; + } + + private boolean differentFromParents(Invokable invokable, TypeToken type) { + Class superclass = type.getRawType().getSuperclass(); + if (superclass != null) { + var superType = TypeToken.of(superclass); + if (differentFromParents2(invokable, superType)) { + return true; + } + } + for (var iface : type.getTypes().interfaces()) { + if (differentFromParents2(invokable, iface)) { + return true; + } + } + return false; + } + + @Override + public String toString() { + return builder.toString(); + } + + private record LuaFieldDefinition(String name, String type) { + + void write(StringBuilder builder) { + builder.append("---@field %s %s%n".formatted(name, type)); + } + } + + private record LuaParameterDefinition(String name, String type) {} + + private record LuaTypeParameter(String name, String superType) { + + @Override + public String toString() { + return name + + ((superType.equals(luaClassName(Object.class)) || "any".equals(superType)) ? "" : (" : " + superType)); + } + } + + private record LuaMethodDefinitions(String name, List typeParameters, + List params, String returnType) { + + void write(String typeName, StringBuilder builder) { + for (var typeParam : typeParameters) { + builder.append("---@generic %s%n".formatted(typeParam)); + } + for (var param : params) { + builder.append("---@param %s %s%n".formatted(param.name, param.type)); + } + builder.append("---@return %s%n".formatted(returnType)); + builder.append("function types.%s:%s(%s) end%n".formatted( + typeName, + name, + params.stream().map(p -> p.name).collect(Collectors.joining(", ")) + )); + } + + public String functionTypeString() { + return "fun(%s): %s".formatted( + params.stream().map(p -> p.name + ": " + p.type).collect(Collectors.joining(", ")), + returnType + ); + } + + public void writeAsField(StringBuilder builder) { + builder.append("---@field %s %s%n".formatted( + name, + functionTypeString() + )); + } + } + + private class LuaTypeDefinition { + + private final TypeToken type; + private final boolean isStatic; + String name; + Set parents = new LinkedHashSet<>(); + Map fields = new TreeMap<>(); + Set methods = new TreeSet<>(Comparator.comparing(Record::toString)); + + LuaTypeDefinition(TypeToken type, String suffix, boolean isStatic) { + this.type = type; + this.name = luaClassName(type.getRawType()) + suffix; + this.isStatic = isStatic; + } + + LuaTypeDefinition(TypeToken type) { + this(type, "", false); + } + + void addParent(TypeToken type) { + parents.add(getInstanceTypeName(type)); + } + + LuaFieldDefinition addField(Member field, Type fieldType) { + try { + debugStack.push("." + field.getName()); + String fieldName = transformMemberName(field.getName()); + var fieldDefinition = + new LuaFieldDefinition(fieldName, getInstanceTypeName(type.resolveType(fieldType))); + fields.put(fieldName, fieldDefinition); + return fieldDefinition; + } finally { + debugStack.pop(); + } + } + + LuaFieldDefinition addField(Field field) { + if (field.getType().equals(LuaValue.class) && field.isAnnotationPresent(LuaFunctionType.class)) { + var functionDetails = field.getAnnotation(LuaFunctionType.class); + var target = functionDetails.target(); + var targetMethod = functionDetails.method().isBlank() ? + CaseFormat.LOWER_UNDERSCORE.to(CaseFormat.LOWER_CAMEL, field.getName()) : + functionDetails.method(); + var matchingMethods = Arrays.stream(target.getDeclaredMethods()) + .filter(m -> m.getName().equals(targetMethod)) + .toList(); + if (matchingMethods.size() != 1) { + throw new IllegalArgumentException("Expected exactly 1 method named " + targetMethod + + " on " + target.getSimpleName() + ", found " + matchingMethods.size() + " " + String.join("", debugStack)); + } + var definition = new LuaTypeDefinition(type); + var method = definition.createMethod(matchingMethods.get(0)); + var fieldName = transformMemberName(field.getName()); + var fieldDefinition = new LuaFieldDefinition( + fieldName, + method.functionTypeString() + ); + fields.put(fieldName, fieldDefinition); + return fieldDefinition; + } + return addField(field, field.getGenericType()); + } + + void addMethod(Method method) { + methods.add(createMethod(method)); + } + + void addMethod(String methodName, Executable method, Type returnType) { + methods.add(createMethod(methodName, method, returnType)); + } + + private LuaMethodDefinitions createMethod(Method method) { + return createMethod(method.getName(), method, method.getGenericReturnType()); + } + + private LuaMethodDefinitions createMethod(String methodName, Executable method, Type returnType) { + methodName = transformMemberName(methodName); + List typeParameters = new ArrayList<>(); + for (var param : method.getTypeParameters()) { + typeParameters.add(new LuaTypeParameter( + param.getName(), + getInstanceTypeName(type.resolveType(param.getBounds()[0])) + )); + } + List parameters = new ArrayList<>(); + for (var param : method.getParameters()) { + parameters.add(new LuaParameterDefinition( + transformMemberName(param.getName()), + param.isAnnotationPresent(LuaType.class) ? param.getAnnotation(LuaType.class).value() : + param.getType() == Path.class ? "%s|string|string[]".formatted(getInstanceTypeName(Path.class)) : + resolveType(param.getParameterizedType(), method.getTypeParameters()) + )); + } + return new LuaMethodDefinitions( + methodName, + typeParameters, + parameters, + resolveType(returnType, method.getTypeParameters()) + ); + } + + private String resolveType(Type elementType, TypeVariable[] typeParameters) { + var resolvedType = type.resolveType(elementType); + // only return type parameter name when it is parameterized at the method level, see: + // https://github.com/LuaLS/lua-language-server/issues/734 + // https://github.com/LuaLS/lua-language-server/issues/1861 + if (resolvedType.getType() instanceof TypeVariable variable) { + for (var typeParam : typeParameters) { + if (typeParam.getName().equals(variable.getName())) { + return typeParam.getName(); + } + } + } + return getInstanceTypeName(resolvedType); + } + + void write(StringBuilder builder) { + String nameToUse = this.name; + if (type.getRawType().isEnum() && !isStatic) { + nameToUse += "__enum"; + builder.append("---@alias %s%n".formatted(name)); + builder.append("---|%s%n".formatted(nameToUse)); + builder.append("---|integer").append(NEWLINE); + for (var constant : type.getRawType().getEnumConstants()) { + builder.append("---|%s%n".formatted(Format.quote(constant.toString()))); + } + } + builder.append("---@class (exact) %s".formatted(nameToUse)); + if (!parents.isEmpty()) { + builder.append(" : ").append(String.join(", ", parents)); + } + builder.append(NEWLINE); + for (var field : fields.values()) { + field.write(builder); + } + boolean bindMethods = type.getRawType().isAnnotationPresent(LuaBindMethods.class); + if (bindMethods) { + for (var method : methods) { + method.writeAsField(builder); + } + } + builder.append("types.%s = {}%n".formatted(nameToUse)); + if (!bindMethods) { + for (var method : methods) { + method.write(nameToUse, builder); + } + } + } + + public String generate() { + StringBuilder tmp = new StringBuilder(); + write(tmp); + return tmp.toString(); + } + + } +} diff --git a/planetiler-experimental/src/main/java/com/onthegomap/planetiler/experimental/lua/JavaToLuaCase.java b/planetiler-experimental/src/main/java/com/onthegomap/planetiler/experimental/lua/JavaToLuaCase.java new file mode 100644 index 0000000000..18f445cacf --- /dev/null +++ b/planetiler-experimental/src/main/java/com/onthegomap/planetiler/experimental/lua/JavaToLuaCase.java @@ -0,0 +1,71 @@ +package com.onthegomap.planetiler.experimental.lua; + +import static java.lang.Character.isDigit; +import static java.lang.Character.isLowerCase; +import static java.lang.Character.isUpperCase; + +import java.util.List; +import java.util.Locale; +import java.util.Objects; +import java.util.Set; +import java.util.regex.Pattern; + +/** + * Converts conventional javaMemberNames to lua_member_names, and lua keywords to uppercase. + */ +public class JavaToLuaCase { + public static final Set LUA_KEYWORDS = Set.of( + "and", "break", "do", "else", "elseif", + "end", "false", "for", "function", "if", + "in", "local", "nil", "not", "or", + "repeat", "return", "then", "true", "until", "while" + ); + private static final String LOWER = "[a-z]"; + private static final String DIGIT = "\\d"; + private static final String UPPER = "[A-Z]"; + private static final String UPPER_OR_DIGIT = "[" + UPPER + DIGIT + "]"; + private static final List BOUNDARIES = List.of( + // getUTF8string -> get_utf8_string + new Boundary(DIGIT, LOWER), + // fooBar -> foo_bar + new Boundary(LOWER, UPPER), + // ASCIIString -> ascii_string, UTF8String -> utf8_string + new Boundary(UPPER_OR_DIGIT, UPPER + LOWER) + ); + private static final List BOUNDARY_PATTERNS = BOUNDARIES.stream() + .map(b -> Pattern.compile("(" + b.prev + ")(" + b.next + ")")) + .toList(); + + public static boolean isLowerCamelCase(String fieldName) { + var chars = fieldName.toCharArray(); + if (!isLowerCase(chars[0])) { + return false; + } + boolean upper = false, lower = false, underscore = false; + for (char c : chars) { + upper |= isUpperCase(c) || isDigit(c); + lower |= isLowerCase(c); + underscore |= c == '_'; + } + return upper && lower && !underscore; + } + + public static String transformMemberName(String fieldName) { + if (isLowerCamelCase(fieldName)) { + fieldName = camelToSnake(fieldName); + } + if (LUA_KEYWORDS.contains(fieldName)) { + fieldName = Objects.requireNonNull(fieldName).toUpperCase(Locale.ROOT); + } + return fieldName; + } + + private static String camelToSnake(String fieldName) { + for (Pattern pattern : BOUNDARY_PATTERNS) { + fieldName = pattern.matcher(fieldName).replaceAll("$1_$2"); + } + return fieldName.toLowerCase(); + } + + private record Boundary(String prev, String next) {} +} diff --git a/planetiler-experimental/src/main/java/com/onthegomap/planetiler/experimental/lua/LuaConversions.java b/planetiler-experimental/src/main/java/com/onthegomap/planetiler/experimental/lua/LuaConversions.java new file mode 100644 index 0000000000..8dd7e41665 --- /dev/null +++ b/planetiler-experimental/src/main/java/com/onthegomap/planetiler/experimental/lua/LuaConversions.java @@ -0,0 +1,206 @@ +package com.onthegomap.planetiler.experimental.lua; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.stream.Stream; +import org.luaj.vm2.LuaTable; +import org.luaj.vm2.LuaTables; +import org.luaj.vm2.LuaUserdata; +import org.luaj.vm2.LuaValue; +import org.luaj.vm2.lib.OneArgFunction; +import org.luaj.vm2.lib.jse.CoerceJavaToLua; +import org.luaj.vm2.lib.jse.CoerceLuaToJava; + +/** + * Helper methods to convert between lua and java types. + */ +public interface LuaConversions { + + static LuaValue toLua(Object sourceFeature) { + return CoerceJavaToLua.coerce(sourceFeature); + } + + static LuaTable toLuaTable(Collection list) { + return LuaValue.listOf(list.stream().map(LuaConversions::toLua).toArray(LuaValue[]::new)); + } + + static LuaTable toLuaTable(Map map) { + return LuaValue.tableOf(map.entrySet().stream() + .flatMap(entry -> Stream.of(toLua(entry.getKey()), toLua(entry.getValue()))) + .toArray(LuaValue[]::new)); + } + + @SuppressWarnings("unchecked") + static T toJava(LuaValue value, Class clazz) { + return (T) CoerceLuaToJava.coerce(value, clazz); + } + + static List toJavaList(LuaValue list) { + return toJavaList(list, Object.class); + } + + static List toJavaList(LuaValue list, Class itemClass) { + if (list instanceof LuaUserdata userdata && userdata.userdata() instanceof List fromLua) { + @SuppressWarnings("unchecked") List result = (List) fromLua; + return result; + } else if (list.istable()) { + int length = list.length(); + List result = new ArrayList<>(); + for (int i = 0; i < length; i++) { + result.add(toJava(list.get(i + 1), itemClass)); + } + return result; + } + return List.of(); + } + + static Collection toJavaCollection(LuaValue list) { + return toJavaCollection(list, Object.class); + } + + static Collection toJavaCollection(LuaValue list, Class itemClass) { + if (list instanceof LuaUserdata userdata && userdata.userdata() instanceof Collection fromLua) { + @SuppressWarnings("unchecked") Collection result = (Collection) fromLua; + return result; + } else { + return toJavaList(list, itemClass); + } + } + + static Iterable toJavaIterable(LuaValue list) { + return toJavaIterable(list, Object.class); + } + + static Iterable toJavaIterable(LuaValue list, Class itemClass) { + if (list instanceof LuaUserdata userdata && userdata.userdata() instanceof Iterable fromLua) { + @SuppressWarnings("unchecked") Iterable result = (Iterable) fromLua; + return result; + } else { + return toJavaList(list, itemClass); + } + } + + static Set toJavaSet(LuaValue list) { + return toJavaSet(list, Object.class); + } + + static Set toJavaSet(LuaValue list, Class itemClass) { + if (list instanceof LuaUserdata userdata && userdata.userdata() instanceof Set fromLua) { + @SuppressWarnings("unchecked") Set result = (Set) fromLua; + return result; + } else if (list instanceof LuaTable table) { + Set result = new LinkedHashSet<>(); + if (LuaTables.isArray(table)) { + int length = list.length(); + for (int i = 0; i < length; i++) { + result.add(toJava(list.get(i + 1), itemClass)); + } + } else { + for (var key : table.keys()) { + var value = table.get(key); + if (value.toboolean()) { + result.add(toJava(key, itemClass)); + } + } + } + return result; + } + return Set.of(); + } + + static Map toJavaMap(LuaValue list) { + return toJavaMap(list, Object.class, Object.class); + } + + static Map toJavaMap(LuaValue list, Class keyClass, Class valueClass) { + if (list instanceof LuaUserdata userdata && userdata.userdata() instanceof Map fromLua) { + @SuppressWarnings("unchecked") Map result = (Map) fromLua; + return result; + } else if (list instanceof LuaTable table) { + Map result = new LinkedHashMap<>(); + for (var key : table.keys()) { + result.put(toJava(key, keyClass), toJava(table.get(key), valueClass)); + } + return result; + } + return Map.of(); + } + + static LuaValue consumerToLua(Consumer consumer, Class itemClass) { + return new LuaConsumer<>(consumer, itemClass); + } + + class LuaConsumer extends OneArgFunction { + + private final Class itemClass; + private final Consumer consumer; + + public LuaConsumer(Consumer consumer, Class itemClass) { + this.consumer = consumer; + this.itemClass = itemClass; + } + + @Override + public LuaValue call(LuaValue arg) { + consumer.accept(toJava(arg, itemClass)); + return NIL; + } + + @Override + public boolean equals(Object o) { + return this == o || + (o instanceof LuaConsumer other && + itemClass.equals(other.itemClass) && + consumer.equals(other.consumer)); + } + + @Override + public int hashCode() { + int result = itemClass.hashCode(); + result = 31 * result + consumer.hashCode(); + return result; + } + } + + static LuaValue functionToLua(Function fn, Class inputClass) { + return new FunctionWrapper<>(fn, inputClass); + } + + class FunctionWrapper extends OneArgFunction { + + private final Class inputClass; + private final Function fn; + + public FunctionWrapper(Function fn, Class inputClass) { + this.fn = fn; + this.inputClass = inputClass; + } + + @Override + public LuaValue call(LuaValue arg) { + return toLua(fn.apply(toJava(arg, inputClass))); + } + + @Override + public boolean equals(Object o) { + return this == o || + (o instanceof FunctionWrapper other && + inputClass.equals(other.inputClass) && + fn.equals(other.fn)); + } + + @Override + public int hashCode() { + int result = inputClass.hashCode(); + result = 31 * result + fn.hashCode(); + return result; + } + } +} diff --git a/planetiler-experimental/src/main/java/com/onthegomap/planetiler/experimental/lua/LuaEnvironment.java b/planetiler-experimental/src/main/java/com/onthegomap/planetiler/experimental/lua/LuaEnvironment.java new file mode 100644 index 0000000000..b52aebb411 --- /dev/null +++ b/planetiler-experimental/src/main/java/com/onthegomap/planetiler/experimental/lua/LuaEnvironment.java @@ -0,0 +1,271 @@ +package com.onthegomap.planetiler.experimental.lua; + +import static com.onthegomap.planetiler.experimental.lua.LuaConversions.toJava; +import static com.onthegomap.planetiler.experimental.lua.LuaConversions.toJavaMap; +import static com.onthegomap.planetiler.experimental.lua.LuaConversions.toLua; + +import com.onthegomap.planetiler.FeatureMerge; +import com.onthegomap.planetiler.Planetiler; +import com.onthegomap.planetiler.config.Arguments; +import com.onthegomap.planetiler.config.PlanetilerConfig; +import com.onthegomap.planetiler.expression.Expression; +import com.onthegomap.planetiler.expression.MultiExpression; +import com.onthegomap.planetiler.geo.GeoUtils; +import com.onthegomap.planetiler.stats.Stats; +import com.onthegomap.planetiler.util.BuildInfo; +import com.onthegomap.planetiler.util.LanguageUtils; +import com.onthegomap.planetiler.util.Parse; +import com.onthegomap.planetiler.util.SortKey; +import com.onthegomap.planetiler.util.Translations; +import com.onthegomap.planetiler.util.ZoomFunction; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.UncheckedIOException; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import org.luaj.vm2.Globals; +import org.luaj.vm2.LuaValue; +import org.luaj.vm2.lib.jse.ExtraPlanetilerCoercions; +import org.luaj.vm2.lib.jse.JsePlatform; +import org.luaj.vm2.lib.jse.LuaBindMethods; +import org.luaj.vm2.lib.jse.LuaFunctionType; +import org.luaj.vm2.lib.jse.LuaGetter; +import org.luaj.vm2.lib.jse.LuaSetter; +import org.luaj.vm2.lib.jse.LuaType; +import org.luaj.vm2.luajc.LuaJC; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Global variables exposed to lua scripts. + *

+ * All instance fields annotated with {@link ExposeToLua} will be exposed to lua as global variables. + */ +@SuppressWarnings({"java:S1104", "java:S116", "unused", "java:S100"}) +public class LuaEnvironment { + + private static final Logger LOGGER = LoggerFactory.getLogger(LuaEnvironment.class); + static final Set> CLASSES_TO_EXPOSE = Set.of( + ZoomFunction.class, + FeatureMerge.class, + Parse.class, + LanguageUtils.class, + Expression.class, + MultiExpression.class, + GeoUtils.class, + SortKey.class + ); + @ExposeToLua + public final PlanetilerNamespace planetiler; + final Planetiler runner; + public LuaProfile profile; + public LuaValue main; + + public LuaEnvironment(Planetiler runner) { + this.runner = runner; + this.planetiler = new PlanetilerNamespace(); + } + + public static LuaEnvironment loadScript(Arguments arguments, Path script) throws IOException { + return loadScript(arguments, Files.readString(script), script.getFileName().toString(), Map.of(), + ConcurrentHashMap.newKeySet(), script); + } + + public static LuaEnvironment loadScript(Arguments args, Path scriptPath, Set pathsToWatch) throws IOException { + return loadScript(args, Files.readString(scriptPath), scriptPath.getFileName().toString(), Map.of(), pathsToWatch, + scriptPath); + } + + public static LuaEnvironment loadScript(Arguments arguments, String script, String fileName) { + return loadScript(arguments, script, fileName, Map.of(), ConcurrentHashMap.newKeySet(), Path.of(".")); + } + + public static LuaEnvironment loadScript(Arguments arguments, String script, String fileName, Map extras, + Set filesLoaded, Path scriptPath) { + ExtraPlanetilerCoercions.install(); + boolean luajc = arguments.getBoolean("luajc", "compile lua to java bytecode", true); + Globals globals = JsePlatform.standardGlobals(); + if (luajc) { + LuaJC.install(globals); + } + Planetiler runner = Planetiler.create(arguments); + LuaEnvironment env = new LuaEnvironment(runner); + env.install(globals); + extras.forEach((name, java) -> globals.set(name, toLua(java))); + var oldFilder = globals.finder; + globals.finder = filename -> { + Path path = Path.of(filename); + if (!Files.exists(path)) { + path = scriptPath.resolveSibling(filename); + } + filesLoaded.add(path); + return oldFilder.findResource(path.toString()); + }; + // ensure source is treated as UTF-8 + try (var in = new ByteArrayInputStream(script.getBytes(StandardCharsets.UTF_8))) { + globals.load(in, fileName, "t", globals).call(); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + LuaProfile profile = new LuaProfile(env); + env.profile = new LuaProfile(env); + env.main = globals.get("main"); + return env; + } + + public static LuaEnvironment loadScript(Arguments args, String script, String filename, Map map) { + return loadScript(args, script, filename, map, ConcurrentHashMap.newKeySet(), Path.of(filename)); + } + + public void run() throws Exception { + runner.setProfile(profile); + if (main != null && main.isfunction()) { + main.call(toLua(runner)); + } else { + runner.overwriteOutput(planetiler.output.path).run(); + } + } + + public void install(Globals globals) { + for (var field : getClass().getDeclaredFields()) { + var annotation = field.getAnnotation(ExposeToLua.class); + if (annotation != null) { + String name = annotation.value().isBlank() ? field.getName() : annotation.value(); + try { + globals.set(name, toLua(field.get(this))); + } catch (IllegalAccessException e) { + throw new IllegalStateException(e); + } + } + } + for (var clazz : CLASSES_TO_EXPOSE) { + globals.set(clazz.getSimpleName(), toLua(clazz)); + } + } + + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.FIELD) + public @interface ExposeToLua { + + String value() default ""; + } + + public static class PlanetilerOutput { + public Path path = Path.of("data", "output.mbtiles"); + public String name; + public String description; + public String attribution; + public String version; + public boolean is_overlay; + } + + @LuaBindMethods + public class PlanetilerNamespace { + public final BuildInfo build = BuildInfo.get(); + public final PlanetilerConfig config = runner.config(); + public final Stats stats = runner.stats(); + public final Arguments args = runner.arguments(); + public final PlanetilerOutput output = new PlanetilerOutput(); + @LuaFunctionType(target = LuaProfile.class) + public LuaValue process_feature; + @LuaFunctionType(target = LuaProfile.class) + public LuaValue cares_about_source; + @LuaFunctionType(target = LuaProfile.class) + public LuaValue cares_about_wikidata_translation; + @LuaFunctionType(target = LuaProfile.class) + public LuaValue estimate_ram_required; + @LuaFunctionType(target = LuaProfile.class) + public LuaValue estimate_intermediate_disk_bytes; + @LuaFunctionType(target = LuaProfile.class) + public LuaValue estimate_output_bytes; + @LuaFunctionType(target = LuaProfile.class) + public LuaValue finish; + @LuaFunctionType(target = LuaProfile.class) + public LuaValue preprocess_osm_node; + @LuaFunctionType(target = LuaProfile.class) + public LuaValue preprocess_osm_way; + @LuaFunctionType(target = LuaProfile.class) + public LuaValue preprocess_osm_relation; + @LuaFunctionType(target = LuaProfile.class) + public LuaValue release; + @LuaFunctionType(target = LuaProfile.class, method = "postProcessLayerFeatures") + public LuaValue post_process; + public String examples; + + private static T get(LuaValue map, String key, Class clazz) { + LuaValue value = map.get(key); + return value.isnil() ? null : toJava(value, clazz); + } + + @LuaGetter + public Translations translations() { + return runner.translations(); + } + + @LuaGetter + public List languages() { + return runner.getDefaultLanguages(); + } + + @LuaSetter + public void languages(List languages) { + runner.setDefaultLanguages(languages); + } + + public void fetch_wikidata_translations(Path defaultPath) { + runner.fetchWikidataNameTranslations(defaultPath); + } + + public void fetch_wikidata_translations() { + runner.fetchWikidataNameTranslations(Path.of("data", "sources", "wikidata_names.json")); + } + + public void add_source( + String name, + @LuaType("{type: 'osm'|'shapefile'|'geopackage'|'naturalearth', path: string|string[], url: string, projection: string, glob: string}") LuaValue map) { + String type = get(map, "type", String.class); + Path path = get(map, "path", Path.class); + if (name == null || type == null) { + throw new IllegalArgumentException("Sources must have 'type', got: " + toJavaMap(map)); + } + String url = get(map, "url", String.class); + String projection = get(map, "projection", String.class); + String glob = get(map, "glob", String.class); + if (path == null) { + if (url == null) { + throw new IllegalArgumentException( + "Sources must have either a 'url' or local 'path', got: " + toJavaMap(map)); + } + String filename = url + .replaceFirst("^https?://", "") + .replaceAll("[\\W&&[^.]]+", "_"); + if (type.equals("osm") && !filename.endsWith(".pbf")) { + filename = filename + ".osm.pbf"; + } + path = Path.of("data", "sources", filename); + } + switch (type) { + case "osm" -> runner.addOsmSource(name, path, url); + case "shapefile" -> { + if (glob != null) { + runner.addShapefileGlobSource(projection, name, path, glob, url); + } else { + runner.addShapefileSource(projection, name, path, url); + } + } + case "geopackage" -> runner.addGeoPackageSource(projection, name, path, url); + case "natural_earth" -> runner.addNaturalEarthSource(name, path, url); + default -> throw new IllegalArgumentException("Unrecognized source type: " + type); + } + } + } +} diff --git a/planetiler-experimental/src/main/java/com/onthegomap/planetiler/experimental/lua/LuaMain.java b/planetiler-experimental/src/main/java/com/onthegomap/planetiler/experimental/lua/LuaMain.java new file mode 100644 index 0000000000..866066c440 --- /dev/null +++ b/planetiler-experimental/src/main/java/com/onthegomap/planetiler/experimental/lua/LuaMain.java @@ -0,0 +1,21 @@ +package com.onthegomap.planetiler.experimental.lua; + +import com.onthegomap.planetiler.config.Arguments; +import java.nio.file.Path; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Main entrypoint for running a lua profile. + */ +public class LuaMain { + private static final Logger LOGGER = LoggerFactory.getLogger(LuaMain.class); + + public static void main(String... args) throws Exception { + LOGGER.warn( + "Lua profiles are experimental and may change! Please provide feedback and report any bugs before depending on it in production."); + var arguments = Arguments.fromEnvOrArgs(args); + Path script = arguments.inputFile("script", "the lua script to run", Path.of("profile.lua")); + LuaEnvironment.loadScript(arguments, script).run(); + } +} diff --git a/planetiler-experimental/src/main/java/com/onthegomap/planetiler/experimental/lua/LuaProfile.java b/planetiler-experimental/src/main/java/com/onthegomap/planetiler/experimental/lua/LuaProfile.java new file mode 100644 index 0000000000..26a709d942 --- /dev/null +++ b/planetiler-experimental/src/main/java/com/onthegomap/planetiler/experimental/lua/LuaProfile.java @@ -0,0 +1,142 @@ +package com.onthegomap.planetiler.experimental.lua; + +import com.onthegomap.planetiler.FeatureCollector; +import com.onthegomap.planetiler.Profile; +import com.onthegomap.planetiler.VectorTile; +import com.onthegomap.planetiler.reader.SourceFeature; +import com.onthegomap.planetiler.reader.osm.OsmElement; +import com.onthegomap.planetiler.reader.osm.OsmRelationInfo; +import java.util.List; +import java.util.function.Consumer; +import org.luaj.vm2.LuaInteger; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Implementation of {@link Profile} that delegates to a lua script. + */ +@SuppressWarnings("java:S1168") +public class LuaProfile implements Profile { + + private static final Logger LOGGER = LoggerFactory.getLogger(LuaProfile.class); + private final LuaEnvironment.PlanetilerNamespace planetiler; + + public LuaProfile(LuaEnvironment env) { + this.planetiler = env.planetiler; + if (planetiler.process_feature == null) { + LOGGER.warn("Missing function planetiler.process_feature"); + } + } + + @Override + public void processFeature(SourceFeature sourceFeature, FeatureCollector features) { + if (planetiler.process_feature != null) { + planetiler.process_feature.call(LuaConversions.toLua(sourceFeature), LuaConversions.toLua(features)); + } + } + + @Override + public List postProcessLayerFeatures(String layer, int zoom, List items) { + if (planetiler.post_process != null) { + return LuaConversions.toJavaList( + planetiler.post_process.call(LuaConversions.toLua(layer), LuaConversions.toLua(zoom), + LuaConversions.toLua(items)), + VectorTile.Feature.class); + } + return null; + } + + @Override + public boolean caresAboutSource(String name) { + return planetiler.cares_about_source == null || planetiler.cares_about_source.call(name).toboolean(); + } + + @Override + public boolean caresAboutWikidataTranslation(OsmElement elem) { + return planetiler.cares_about_wikidata_translation != null && + planetiler.cares_about_wikidata_translation.call(LuaConversions.toLua(elem)).toboolean(); + } + + @Override + public String name() { + return planetiler.output.name; + } + + @Override + public String description() { + return planetiler.output.description; + } + + @Override + public String attribution() { + return planetiler.output.attribution; + } + + @Override + public String version() { + return planetiler.output.version; + } + + + @Override + public boolean isOverlay() { + return planetiler.output.is_overlay; + } + + @Override + public void finish(String sourceName, FeatureCollector.Factory featureCollectors, + Consumer next) { + if (planetiler.finish != null) { + planetiler.finish.call(LuaConversions.toLua(sourceName), LuaConversions.toLua(featureCollectors), + LuaConversions.consumerToLua(next, FeatureCollector.Feature.class)); + } + } + + @Override + public long estimateIntermediateDiskBytes(long osmFileSize) { + return planetiler.estimate_intermediate_disk_bytes == null ? 0 : + planetiler.estimate_intermediate_disk_bytes.call(LuaInteger.valueOf(osmFileSize)).tolong(); + } + + @Override + public long estimateOutputBytes(long osmFileSize) { + return planetiler.estimate_output_bytes == null ? 0 : + planetiler.estimate_output_bytes.call(LuaInteger.valueOf(osmFileSize)).tolong(); + } + + @Override + public long estimateRamRequired(long osmFileSize) { + return planetiler.estimate_ram_required == null ? 0 : + planetiler.estimate_ram_required.call(LuaInteger.valueOf(osmFileSize)).tolong(); + } + + @Override + public void preprocessOsmNode(OsmElement.Node node) { + if (planetiler.preprocess_osm_node != null) { + planetiler.preprocess_osm_node.call(LuaConversions.toLua(node)); + } + } + + @Override + public void preprocessOsmWay(OsmElement.Way way) { + if (planetiler.preprocess_osm_way != null) { + planetiler.preprocess_osm_way.call(LuaConversions.toLua(way)); + } + } + + @Override + public List preprocessOsmRelation(OsmElement.Relation relation) { + if (planetiler.preprocess_osm_relation == null) { + return null; + } + return LuaConversions.toJavaList(planetiler.preprocess_osm_relation.call(LuaConversions.toLua(relation)), + OsmRelationInfo.class); + } + + @Override + public void release() { + if (planetiler.release != null) { + planetiler.release.call(); + } + } +} diff --git a/planetiler-experimental/src/main/java/com/onthegomap/planetiler/experimental/lua/LuaValidator.java b/planetiler-experimental/src/main/java/com/onthegomap/planetiler/experimental/lua/LuaValidator.java new file mode 100644 index 0000000000..5291823ad7 --- /dev/null +++ b/planetiler-experimental/src/main/java/com/onthegomap/planetiler/experimental/lua/LuaValidator.java @@ -0,0 +1,78 @@ +package com.onthegomap.planetiler.experimental.lua; + +import com.fasterxml.jackson.core.JacksonException; +import com.onthegomap.planetiler.config.Arguments; +import com.onthegomap.planetiler.config.PlanetilerConfig; +import com.onthegomap.planetiler.util.AnsiColors; +import com.onthegomap.planetiler.validator.BaseSchemaValidator; +import com.onthegomap.planetiler.validator.SchemaSpecification; +import java.io.PrintStream; +import java.nio.file.Path; +import java.util.List; +import java.util.Set; +import java.util.stream.Stream; +import org.apache.commons.lang3.exception.ExceptionUtils; +import org.snakeyaml.engine.v2.exceptions.YamlEngineException; + +/** + * Validates a lua profile against a yaml set of example source features and the vector tile features they should map to + **/ +public class LuaValidator extends BaseSchemaValidator { + + private final Path scriptPath; + + LuaValidator(Arguments args, String schemaFile, PrintStream output) { + super(args, output); + scriptPath = schemaFile == null ? args.inputFile("script", "Schema file") : + args.inputFile("script", "Script file", Path.of(schemaFile)); + } + + public static void main(String[] args) { + // let users run `verify schema.lua` as a shortcut + String schemaFile = null; + if (args.length > 0 && args[0].endsWith(".lua") && !args[0].startsWith("-")) { + schemaFile = args[0]; + args = Stream.of(args).skip(1).toArray(String[]::new); + } + var arguments = Arguments.fromEnvOrArgs(args).silence(); + new LuaValidator(arguments, schemaFile, System.out).runOrWatch(); + } + + @Override + protected Result validate(Set pathsToWatch) { + Result result = null; + try { + pathsToWatch.add(scriptPath); + var env = LuaEnvironment.loadScript(args, scriptPath, pathsToWatch); + // examples can either be embedded in the lua file, or referenced + Path specPath; + if (env.planetiler.examples != null) { + specPath = Path.of(env.planetiler.examples); + if (!specPath.isAbsolute()) { + specPath = scriptPath.resolveSibling(specPath); + } + } else { + specPath = args.file("spec", "yaml spec", null); + } + SchemaSpecification spec; + if (specPath != null) { + pathsToWatch.add(specPath); + spec = SchemaSpecification.load(specPath); + } else { + spec = new SchemaSpecification(List.of()); + } + result = validate(env.profile, spec, PlanetilerConfig.from(args)); + } catch (Exception exception) { + Throwable rootCause = ExceptionUtils.getRootCause(exception); + if (hasCause(exception, YamlEngineException.class) || hasCause(exception, JacksonException.class)) { + output.println(AnsiColors.red("Malformed yaml input:\n\n" + rootCause.toString().indent(4))); + } else { + output.println(AnsiColors.red( + "Unexpected exception thrown:\n" + rootCause.toString().indent(4) + "\n" + + String.join("\n", ExceptionUtils.getStackTrace(rootCause))) + .indent(4)); + } + } + return result; + } +} diff --git a/planetiler-experimental/src/main/java/org/luaj/vm2/LuaInteger.java b/planetiler-experimental/src/main/java/org/luaj/vm2/LuaInteger.java new file mode 100644 index 0000000000..c7c4fece94 --- /dev/null +++ b/planetiler-experimental/src/main/java/org/luaj/vm2/LuaInteger.java @@ -0,0 +1,431 @@ +/******************************************************************************* + * Copyright (c) 2009 Luaj.org. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + ******************************************************************************/ +package org.luaj.vm2; + +import org.luaj.vm2.lib.MathLib; + +/** + * Modified version of {@link LuaInteger} with race condition fixes. + *

+ * Extension of {@link LuaNumber} which can hold a Java int as its value. + *

+ * These instance are not instantiated directly by clients, but indirectly via the static functions + * {@link LuaValue#valueOf(int)} or {@link LuaValue#valueOf(double)} functions. This ensures that policies regarding + * pooling of instances are encapsulated. + *

+ * There are no API's specific to LuaInteger that are useful beyond what is already exposed in {@link LuaValue}. + * + * @see LuaValue + * @see LuaNumber + * @see LuaDouble + * @see LuaValue#valueOf(int) + * @see LuaValue#valueOf(double) + */ +public class LuaInteger extends LuaNumber { + // planetiler change: move int values into nested class to avoid race condition + // since LuaInteger extends LuaValue, but LuaValue calls LuaInteger.valueOf + private static class IntValues { + + private static final LuaInteger[] intValues = new LuaInteger[512]; + static { + for (int i = 0; i < 512; i++) + intValues[i] = new LuaInteger(i - 256); + } + } + + public static LuaInteger valueOf(int i) { + return i <= 255 && i >= -256 ? IntValues.intValues[i + 256] : new LuaInteger(i); + }; + + // TODO consider moving this to LuaValue + /** + * Return a LuaNumber that represents the value provided + * + * @param l long value to represent. + * @return LuaNumber that is eithe LuaInteger or LuaDouble representing l + * @see LuaValue#valueOf(int) + * @see LuaValue#valueOf(double) + */ + public static LuaNumber valueOf(long l) { + int i = (int) l; + return l == i ? (i <= 255 && i >= -256 ? IntValues.intValues[i + 256] : + (LuaNumber) new LuaInteger(i)) : + (LuaNumber) LuaDouble.valueOf(l); + } + + /** The value being held by this instance. */ + public final int v; + + /** + * Package protected constructor. + * + * @see LuaValue#valueOf(int) + **/ + LuaInteger(int i) { + this.v = i; + } + + public boolean isint() { + return true; + } + + public boolean isinttype() { + return true; + } + + public boolean islong() { + return true; + } + + public byte tobyte() { + return (byte) v; + } + + public char tochar() { + return (char) v; + } + + public double todouble() { + return v; + } + + public float tofloat() { + return v; + } + + public int toint() { + return v; + } + + public long tolong() { + return v; + } + + public short toshort() { + return (short) v; + } + + public double optdouble(double defval) { + return v; + } + + public int optint(int defval) { + return v; + } + + public LuaInteger optinteger(LuaInteger defval) { + return this; + } + + public long optlong(long defval) { + return v; + } + + public String tojstring() { + return Integer.toString(v); + } + + public LuaString strvalue() { + return LuaString.valueOf(Integer.toString(v)); + } + + public LuaString optstring(LuaString defval) { + return LuaString.valueOf(Integer.toString(v)); + } + + public LuaValue tostring() { + return LuaString.valueOf(Integer.toString(v)); + } + + public String optjstring(String defval) { + return Integer.toString(v); + } + + public LuaInteger checkinteger() { + return this; + } + + public boolean isstring() { + return true; + } + + public int hashCode() { + return v; + } + + public static int hashCode(int x) { + return x; + } + + // unary operators + public LuaValue neg() { + return valueOf(-(long) v); + } + + // object equality, used for key comparison + public boolean equals(Object o) { + return o instanceof LuaInteger ? ((LuaInteger) o).v == v : false; + } + + // equality w/ metatable processing + public LuaValue eq(LuaValue val) { + return val.raweq(v) ? TRUE : FALSE; + } + + public boolean eq_b(LuaValue val) { + return val.raweq(v); + } + + // equality w/o metatable processing + public boolean raweq(LuaValue val) { + return val.raweq(v); + } + + public boolean raweq(double val) { + return v == val; + } + + public boolean raweq(int val) { + return v == val; + } + + // arithmetic operators + public LuaValue add(LuaValue rhs) { + return rhs.add(v); + } + + public LuaValue add(double lhs) { + return LuaDouble.valueOf(lhs + v); + } + + public LuaValue add(int lhs) { + return LuaInteger.valueOf(lhs + (long) v); + } + + public LuaValue sub(LuaValue rhs) { + return rhs.subFrom(v); + } + + public LuaValue sub(double rhs) { + return LuaDouble.valueOf(v - rhs); + } + + public LuaValue sub(int rhs) { + return LuaDouble.valueOf(v - rhs); + } + + public LuaValue subFrom(double lhs) { + return LuaDouble.valueOf(lhs - v); + } + + public LuaValue subFrom(int lhs) { + return LuaInteger.valueOf(lhs - (long) v); + } + + public LuaValue mul(LuaValue rhs) { + return rhs.mul(v); + } + + public LuaValue mul(double lhs) { + return LuaDouble.valueOf(lhs * v); + } + + public LuaValue mul(int lhs) { + return LuaInteger.valueOf(lhs * (long) v); + } + + public LuaValue pow(LuaValue rhs) { + return rhs.powWith(v); + } + + public LuaValue pow(double rhs) { + return MathLib.dpow(v, rhs); + } + + public LuaValue pow(int rhs) { + return MathLib.dpow(v, rhs); + } + + public LuaValue powWith(double lhs) { + return MathLib.dpow(lhs, v); + } + + public LuaValue powWith(int lhs) { + return MathLib.dpow(lhs, v); + } + + public LuaValue div(LuaValue rhs) { + return rhs.divInto(v); + } + + public LuaValue div(double rhs) { + return LuaDouble.ddiv(v, rhs); + } + + public LuaValue div(int rhs) { + return LuaDouble.ddiv(v, rhs); + } + + public LuaValue divInto(double lhs) { + return LuaDouble.ddiv(lhs, v); + } + + public LuaValue mod(LuaValue rhs) { + return rhs.modFrom(v); + } + + public LuaValue mod(double rhs) { + return LuaDouble.dmod(v, rhs); + } + + public LuaValue mod(int rhs) { + return LuaDouble.dmod(v, rhs); + } + + public LuaValue modFrom(double lhs) { + return LuaDouble.dmod(lhs, v); + } + + // relational operators + public LuaValue lt(LuaValue rhs) { + return rhs.gt_b(v) ? TRUE : FALSE; + } + + public LuaValue lt(double rhs) { + return v < rhs ? TRUE : FALSE; + } + + public LuaValue lt(int rhs) { + return v < rhs ? TRUE : FALSE; + } + + public boolean lt_b(LuaValue rhs) { + return rhs.gt_b(v); + } + + public boolean lt_b(int rhs) { + return v < rhs; + } + + public boolean lt_b(double rhs) { + return v < rhs; + } + + public LuaValue lteq(LuaValue rhs) { + return rhs.gteq_b(v) ? TRUE : FALSE; + } + + public LuaValue lteq(double rhs) { + return v <= rhs ? TRUE : FALSE; + } + + public LuaValue lteq(int rhs) { + return v <= rhs ? TRUE : FALSE; + } + + public boolean lteq_b(LuaValue rhs) { + return rhs.gteq_b(v); + } + + public boolean lteq_b(int rhs) { + return v <= rhs; + } + + public boolean lteq_b(double rhs) { + return v <= rhs; + } + + public LuaValue gt(LuaValue rhs) { + return rhs.lt_b(v) ? TRUE : FALSE; + } + + public LuaValue gt(double rhs) { + return v > rhs ? TRUE : FALSE; + } + + public LuaValue gt(int rhs) { + return v > rhs ? TRUE : FALSE; + } + + public boolean gt_b(LuaValue rhs) { + return rhs.lt_b(v); + } + + public boolean gt_b(int rhs) { + return v > rhs; + } + + public boolean gt_b(double rhs) { + return v > rhs; + } + + public LuaValue gteq(LuaValue rhs) { + return rhs.lteq_b(v) ? TRUE : FALSE; + } + + public LuaValue gteq(double rhs) { + return v >= rhs ? TRUE : FALSE; + } + + public LuaValue gteq(int rhs) { + return v >= rhs ? TRUE : FALSE; + } + + public boolean gteq_b(LuaValue rhs) { + return rhs.lteq_b(v); + } + + public boolean gteq_b(int rhs) { + return v >= rhs; + } + + public boolean gteq_b(double rhs) { + return v >= rhs; + } + + // string comparison + public int strcmp(LuaString rhs) { + typerror("attempt to compare number with string"); + return 0; + } + + public int checkint() { + return v; + } + + public long checklong() { + return v; + } + + public double checkdouble() { + return v; + } + + public String checkjstring() { + return String.valueOf(v); + } + + public LuaString checkstring() { + return valueOf(String.valueOf(v)); + } + +} diff --git a/planetiler-experimental/src/main/java/org/luaj/vm2/LuaString.java b/planetiler-experimental/src/main/java/org/luaj/vm2/LuaString.java new file mode 100644 index 0000000000..c81b1e2e2b --- /dev/null +++ b/planetiler-experimental/src/main/java/org/luaj/vm2/LuaString.java @@ -0,0 +1,1083 @@ +/******************************************************************************* + * Copyright (c) 2009-2011 Luaj.org. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + ******************************************************************************/ +package org.luaj.vm2; + +import java.io.ByteArrayInputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.PrintStream; +import java.nio.charset.StandardCharsets; +import org.luaj.vm2.lib.MathLib; + +/** + * Modified version of LuaString that includes some performance improvements and bug fixes. + *

+ * Subclass of {@link LuaValue} for representing lua strings. + *

+ * Because lua string values are more nearly sequences of bytes than sequences of characters or unicode code points, the + * {@link LuaString} implementation holds the string value in an internal byte array. + *

+ * {@link LuaString} values are not considered mutable once constructed, so multiple {@link LuaString} values can chare + * a single byte array. + *

+ * Currently {@link LuaString}s are pooled via a centrally managed weak table. To ensure that as many string values as + * possible take advantage of this, Constructors are not exposed directly. As with number, booleans, and nil, instance + * construction should be via {@link LuaValue#valueOf(byte[])} or similar API. + *

+ * Because of this pooling, users of LuaString must not directly alter the bytes in a LuaString, or undefined + * behavior will result. + *

+ * When Java Strings are used to initialize {@link LuaString} data, the UTF8 encoding is assumed. The functions + * {@link #lengthAsUtf8(char[])}, {@link #encodeToUtf8(char[], int, byte[], int)}, and + * {@link #decodeAsUtf8(byte[], int, int)} are used to convert back and forth between UTF8 byte arrays and character + * arrays. + * + * @see LuaValue + * @see LuaValue#valueOf(String) + * @see LuaValue#valueOf(byte[]) + */ +public class LuaString extends LuaValue { + + /** + * The singleton instance for string metatables that forwards to the string functions. Typically, this is set to the + * string metatable as a side effect of loading the string library, and is read-write to provide flexible behavior by + * default. When used in a server environment where there may be roge scripts, this should be replaced with a + * read-only table since it is shared across all lua code in this Java VM. + */ + public static LuaValue s_metatable; + + /** + * The bytes for the string. These must not be mutated directly because the backing may be shared by + * multiple LuaStrings, and the hash code is computed only at construction time. It is exposed only for performance + * and legacy reasons. + */ + public final byte[] m_bytes; + + /** The offset into the byte array, 0 means start at the first byte */ + public final int m_offset; + + /** The number of bytes that comprise this string */ + public final int m_length; + + /** The hashcode for this string. Computed at construct time. */ + private final int m_hashcode; + // planetiler change: cache some values that are expensive to recompute + private volatile String cached_string; + private volatile Boolean cached_utf8; + + // planetiler change: cache more strings + /** + * Size of cache of recent short strings. This is the maximum number of LuaStrings that will be retained in the cache + * of recent short strings. Exposed to package for testing. + */ + static final int RECENT_STRINGS_CACHE_SIZE = 4096; + + /** + * Maximum length of a string to be considered for recent short strings caching. This effectively limits the total + * memory that can be spent on the recent strings cache, because no LuaString whose backing exceeds this length will + * be put into the cache. Exposed to package for testing. + */ + static final int RECENT_STRINGS_MAX_LENGTH = 32; + + /** + * Simple cache of recently created strings that are short. This is simply a list of strings, indexed by their hash + * codes modulo the cache size that have been recently constructed. If a string is being constructed frequently from + * different contexts, it will generally show up as a cache hit and resolve to the same value. + */ + private static final class RecentShortStrings { + private static final LuaString recent_short_strings[] = + new LuaString[RECENT_STRINGS_CACHE_SIZE]; + } + + /** + * Get a {@link LuaString} instance whose bytes match the supplied Java String using the UTF8 encoding. + * + * @param string Java String containing characters to encode as UTF8 + * @return {@link LuaString} with UTF8 bytes corresponding to the supplied String + */ + public static LuaString valueOf(String string) { + char[] c = string.toCharArray(); + byte[] b = new byte[lengthAsUtf8(c)]; + encodeToUtf8(c, c.length, b, 0); + return valueUsing(b, 0, b.length); + } + + /** + * Construct a {@link LuaString} for a portion of a byte array. + *

+ * The array is first be used as the backing for this object, so clients must not change contents. If the supplied + * value for 'len' is more than half the length of the container, the supplied byte array will be used as the backing, + * otherwise the bytes will be copied to a new byte array, and cache lookup may be performed. + *

+ * + * @param bytes byte buffer + * @param off offset into the byte buffer + * @param len length of the byte buffer + * @return {@link LuaString} wrapping the byte buffer + */ + public static LuaString valueOf(byte[] bytes, int off, int len) { + if (len > RECENT_STRINGS_MAX_LENGTH) + return valueFromCopy(bytes, off, len); + final int hash = hashCode(bytes, off, len); + final int bucket = hash & (RECENT_STRINGS_CACHE_SIZE - 1); + final LuaString t = RecentShortStrings.recent_short_strings[bucket]; + if (t != null && t.m_hashcode == hash && t.byteseq(bytes, off, len)) + return t; + final LuaString s = valueFromCopy(bytes, off, len); + RecentShortStrings.recent_short_strings[bucket] = s; + return s; + } + + /** Construct a new LuaString using a copy of the bytes array supplied */ + private static LuaString valueFromCopy(byte[] bytes, int off, int len) { + final byte[] copy = new byte[len]; + for (int i = 0; i < len; ++i) + copy[i] = bytes[off + i]; + return new LuaString(copy, 0, len); + } + + /** + * Construct a {@link LuaString} around, possibly using the the supplied byte array as the backing store. + *

+ * The caller must ensure that the array is not mutated after the call. However, if the string is short enough the + * short-string cache is checked for a match which may be used instead of the supplied byte array. + *

+ * + * @param bytes byte buffer + * @return {@link LuaString} wrapping the byte buffer, or an equivalent string. + */ + static public LuaString valueUsing(byte[] bytes, int off, int len) { + if (bytes.length > RECENT_STRINGS_MAX_LENGTH) + return new LuaString(bytes, off, len); + final int hash = hashCode(bytes, off, len); + final int bucket = hash & (RECENT_STRINGS_CACHE_SIZE - 1); + final LuaString t = RecentShortStrings.recent_short_strings[bucket]; + if (t != null && t.m_hashcode == hash && t.byteseq(bytes, off, len)) + return t; + final LuaString s = new LuaString(bytes, off, len); + RecentShortStrings.recent_short_strings[bucket] = s; + return s; + } + + /** + * Construct a {@link LuaString} using the supplied characters as byte values. + *

+ * Only the low-order 8-bits of each character are used, the remainder is ignored. + *

+ * This is most useful for constructing byte sequences that do not conform to UTF8. + * + * @param bytes array of char, whose values are truncated at 8-bits each and put into a byte array. + * @return {@link LuaString} wrapping a copy of the byte buffer + */ + public static LuaString valueOf(char[] bytes) { + return valueOf(bytes, 0, bytes.length); + } + + /** + * Construct a {@link LuaString} using the supplied characters as byte values. + *

+ * Only the low-order 8-bits of each character are used, the remainder is ignored. + *

+ * This is most useful for constructing byte sequences that do not conform to UTF8. + * + * @param bytes array of char, whose values are truncated at 8-bits each and put into a byte array. + * @return {@link LuaString} wrapping a copy of the byte buffer + */ + public static LuaString valueOf(char[] bytes, int off, int len) { + byte[] b = new byte[len]; + for (int i = 0; i < len; i++) + b[i] = (byte) bytes[i + off]; + return valueUsing(b, 0, len); + } + + /** + * Construct a {@link LuaString} for all the bytes in a byte array. + *

+ * The LuaString returned will either be a new LuaString containing a copy of the bytes array, or be an existing + * LuaString used already having the same value. + *

+ * + * @param bytes byte buffer + * @return {@link LuaString} wrapping the byte buffer + */ + public static LuaString valueOf(byte[] bytes) { + return valueOf(bytes, 0, bytes.length); + } + + /** + * Construct a {@link LuaString} for all the bytes in a byte array, possibly using the supplied array as the backing + * store. + *

+ * The LuaString returned will either be a new LuaString containing the byte array, or be an existing LuaString used + * already having the same value. + *

+ * The caller must not mutate the contents of the byte array after this call, as it may be used elsewhere due to + * recent short string caching. + * + * @param bytes byte buffer + * @return {@link LuaString} wrapping the byte buffer + */ + public static LuaString valueUsing(byte[] bytes) { + return valueUsing(bytes, 0, bytes.length); + } + + /** + * Construct a {@link LuaString} around a byte array without copying the contents. + *

+ * The array is used directly after this is called, so clients must not change contents. + *

+ * + * @param bytes byte buffer + * @param offset offset into the byte buffer + * @param length length of the byte buffer + * @return {@link LuaString} wrapping the byte buffer + */ + private LuaString(byte[] bytes, int offset, int length) { + this.m_bytes = bytes; + this.m_offset = offset; + this.m_length = length; + this.m_hashcode = hashCode(bytes, offset, length); + } + + public boolean isstring() { + return true; + } + + public LuaValue getmetatable() { + return s_metatable; + } + + public int type() { + return LuaValue.TSTRING; + } + + public String typename() { + return "string"; + } + + public String tojstring() { + // planetiler change: cache this value + if (cached_string == null) { + synchronized (this) { + if (cached_string == null) { + cached_string = decodeAsUtf8(m_bytes, m_offset, m_length); + } + } + } + return cached_string; + } + + // unary operators + public LuaValue neg() { + double d = scannumber(); + return Double.isNaN(d) ? super.neg() : valueOf(-d); + } + + // basic binary arithmetic + public LuaValue add(LuaValue rhs) { + double d = scannumber(); + return Double.isNaN(d) ? arithmt(ADD, rhs) : rhs.add(d); + } + + public LuaValue add(double rhs) { + return valueOf(checkarith() + rhs); + } + + public LuaValue add(int rhs) { + return valueOf(checkarith() + rhs); + } + + public LuaValue sub(LuaValue rhs) { + double d = scannumber(); + return Double.isNaN(d) ? arithmt(SUB, rhs) : rhs.subFrom(d); + } + + public LuaValue sub(double rhs) { + return valueOf(checkarith() - rhs); + } + + public LuaValue sub(int rhs) { + return valueOf(checkarith() - rhs); + } + + public LuaValue subFrom(double lhs) { + return valueOf(lhs - checkarith()); + } + + public LuaValue mul(LuaValue rhs) { + double d = scannumber(); + return Double.isNaN(d) ? arithmt(MUL, rhs) : rhs.mul(d); + } + + public LuaValue mul(double rhs) { + return valueOf(checkarith() * rhs); + } + + public LuaValue mul(int rhs) { + return valueOf(checkarith() * rhs); + } + + public LuaValue pow(LuaValue rhs) { + double d = scannumber(); + return Double.isNaN(d) ? arithmt(POW, rhs) : rhs.powWith(d); + } + + public LuaValue pow(double rhs) { + return MathLib.dpow(checkarith(), rhs); + } + + public LuaValue pow(int rhs) { + return MathLib.dpow(checkarith(), rhs); + } + + public LuaValue powWith(double lhs) { + return MathLib.dpow(lhs, checkarith()); + } + + public LuaValue powWith(int lhs) { + return MathLib.dpow(lhs, checkarith()); + } + + public LuaValue div(LuaValue rhs) { + double d = scannumber(); + return Double.isNaN(d) ? arithmt(DIV, rhs) : rhs.divInto(d); + } + + public LuaValue div(double rhs) { + return LuaDouble.ddiv(checkarith(), rhs); + } + + public LuaValue div(int rhs) { + return LuaDouble.ddiv(checkarith(), rhs); + } + + public LuaValue divInto(double lhs) { + return LuaDouble.ddiv(lhs, checkarith()); + } + + public LuaValue mod(LuaValue rhs) { + double d = scannumber(); + return Double.isNaN(d) ? arithmt(MOD, rhs) : rhs.modFrom(d); + } + + public LuaValue mod(double rhs) { + return LuaDouble.dmod(checkarith(), rhs); + } + + public LuaValue mod(int rhs) { + return LuaDouble.dmod(checkarith(), rhs); + } + + public LuaValue modFrom(double lhs) { + return LuaDouble.dmod(lhs, checkarith()); + } + + // relational operators, these only work with other strings + public LuaValue lt(LuaValue rhs) { + return rhs.strcmp(this) > 0 ? LuaValue.TRUE : FALSE; + } + + public boolean lt_b(LuaValue rhs) { + return rhs.strcmp(this) > 0; + } + + public boolean lt_b(int rhs) { + typerror("attempt to compare string with number"); + return false; + } + + public boolean lt_b(double rhs) { + typerror("attempt to compare string with number"); + return false; + } + + public LuaValue lteq(LuaValue rhs) { + return rhs.strcmp(this) >= 0 ? LuaValue.TRUE : FALSE; + } + + public boolean lteq_b(LuaValue rhs) { + return rhs.strcmp(this) >= 0; + } + + public boolean lteq_b(int rhs) { + typerror("attempt to compare string with number"); + return false; + } + + public boolean lteq_b(double rhs) { + typerror("attempt to compare string with number"); + return false; + } + + public LuaValue gt(LuaValue rhs) { + return rhs.strcmp(this) < 0 ? LuaValue.TRUE : FALSE; + } + + public boolean gt_b(LuaValue rhs) { + return rhs.strcmp(this) < 0; + } + + public boolean gt_b(int rhs) { + typerror("attempt to compare string with number"); + return false; + } + + public boolean gt_b(double rhs) { + typerror("attempt to compare string with number"); + return false; + } + + public LuaValue gteq(LuaValue rhs) { + return rhs.strcmp(this) <= 0 ? LuaValue.TRUE : FALSE; + } + + public boolean gteq_b(LuaValue rhs) { + return rhs.strcmp(this) <= 0; + } + + public boolean gteq_b(int rhs) { + typerror("attempt to compare string with number"); + return false; + } + + public boolean gteq_b(double rhs) { + typerror("attempt to compare string with number"); + return false; + } + + // concatenation + public LuaValue concat(LuaValue rhs) { + return rhs.concatTo(this); + } + + public Buffer concat(Buffer rhs) { + return rhs.concatTo(this); + } + + public LuaValue concatTo(LuaNumber lhs) { + return concatTo(lhs.strvalue()); + } + + public LuaValue concatTo(LuaString lhs) { + byte[] b = new byte[lhs.m_length + this.m_length]; + System.arraycopy(lhs.m_bytes, lhs.m_offset, b, 0, lhs.m_length); + System.arraycopy(this.m_bytes, this.m_offset, b, lhs.m_length, this.m_length); + return valueUsing(b, 0, b.length); + } + + // string comparison + public int strcmp(LuaValue lhs) { + return -lhs.strcmp(this); + } + + public int strcmp(LuaString rhs) { + for (int i = 0, j = 0; i < m_length && j < rhs.m_length; ++i, ++j) { + if (m_bytes[m_offset + i] != rhs.m_bytes[rhs.m_offset + j]) { + return ((int) m_bytes[m_offset + i]) - ((int) rhs.m_bytes[rhs.m_offset + j]); + } + } + return m_length - rhs.m_length; + } + + /** Check for number in arithmetic, or throw aritherror */ + private double checkarith() { + double d = scannumber(); + if (Double.isNaN(d)) + aritherror(); + return d; + } + + public int checkint() { + return (int) (long) checkdouble(); + } + + public LuaInteger checkinteger() { + return valueOf(checkint()); + } + + public long checklong() { + return (long) checkdouble(); + } + + public double checkdouble() { + double d = scannumber(); + if (Double.isNaN(d)) + argerror("number"); + return d; + } + + public LuaNumber checknumber() { + return valueOf(checkdouble()); + } + + public LuaNumber checknumber(String msg) { + double d = scannumber(); + if (Double.isNaN(d)) + error(msg); + return valueOf(d); + } + + public boolean isnumber() { + double d = scannumber(); + return !Double.isNaN(d); + } + + public boolean isint() { + double d = scannumber(); + if (Double.isNaN(d)) + return false; + int i = (int) d; + return i == d; + } + + public boolean islong() { + double d = scannumber(); + if (Double.isNaN(d)) + return false; + long l = (long) d; + return l == d; + } + + public byte tobyte() { + return (byte) toint(); + } + + public char tochar() { + return (char) toint(); + } + + public double todouble() { + double d = scannumber(); + return Double.isNaN(d) ? 0 : d; + } + + public float tofloat() { + return (float) todouble(); + } + + public int toint() { + return (int) tolong(); + } + + public long tolong() { + return (long) todouble(); + } + + public short toshort() { + return (short) toint(); + } + + public double optdouble(double defval) { + return checknumber().checkdouble(); + } + + public int optint(int defval) { + return checknumber().checkint(); + } + + public LuaInteger optinteger(LuaInteger defval) { + return checknumber().checkinteger(); + } + + public long optlong(long defval) { + return checknumber().checklong(); + } + + public LuaNumber optnumber(LuaNumber defval) { + return checknumber().checknumber(); + } + + public LuaString optstring(LuaString defval) { + return this; + } + + public LuaValue tostring() { + return this; + } + + public String optjstring(String defval) { + return tojstring(); + } + + public LuaString strvalue() { + return this; + } + + /** + * Take a substring using Java zero-based indexes for begin and end or range. + * + * @param beginIndex The zero-based index of the first character to include. + * @param endIndex The zero-based index of position after the last character. + * @return LuaString which is a substring whose first character is at offset beginIndex and extending for (endIndex - + * beginIndex ) characters. + */ + public LuaString substring(int beginIndex, int endIndex) { + final int off = m_offset + beginIndex; + final int len = endIndex - beginIndex; + return len >= m_length / 2 ? + valueUsing(m_bytes, off, len) : + valueOf(m_bytes, off, len); + } + + public int hashCode() { + return m_hashcode; + } + + /** + * Compute the hash code of a sequence of bytes within a byte array using lua's rules for string hashes. For long + * strings, not all bytes are hashed. + * + * @param bytes byte array containing the bytes. + * @param offset offset into the hash for the first byte. + * @param length number of bytes starting with offset that are part of the string. + * @return hash for the string defined by bytes, offset, and length. + */ + public static int hashCode(byte[] bytes, int offset, int length) { + int h = length; /* seed */ + int step = (length >> 5) + 1; /* if string is too long, don't hash all its chars */ + for (int l1 = length; l1 >= step; l1 -= step) /* compute hash */ + h = h ^ ((h << 5) + (h >> 2) + (((int) bytes[offset + l1 - 1]) & 0x0FF)); + return h; + } + + // object comparison, used in key comparison + public boolean equals(Object o) { + if (o instanceof LuaString) { + return raweq((LuaString) o); + } + return false; + } + + // equality w/ metatable processing + public LuaValue eq(LuaValue val) { + return val.raweq(this) ? TRUE : FALSE; + } + + public boolean eq_b(LuaValue val) { + return val.raweq(this); + } + + // equality w/o metatable processing + public boolean raweq(LuaValue val) { + return val.raweq(this); + } + + public boolean raweq(LuaString s) { + if (this == s) + return true; + if (s.m_length != m_length) + return false; + if (s.m_bytes == m_bytes && s.m_offset == m_offset) + return true; + if (s.hashCode() != hashCode()) + return false; + for (int i = 0; i < m_length; i++) + if (s.m_bytes[s.m_offset + i] != m_bytes[m_offset + i]) + return false; + return true; + } + + public static boolean equals(LuaString a, int i, LuaString b, int j, int n) { + return equals(a.m_bytes, a.m_offset + i, b.m_bytes, b.m_offset + j, n); + } + + /** Return true if the bytes in the supplied range match this LuaStrings bytes. */ + private boolean byteseq(byte[] bytes, int off, int len) { + return (m_length == len && equals(m_bytes, m_offset, bytes, off, len)); + } + + public static boolean equals(byte[] a, int i, byte[] b, int j, int n) { + if (a.length < i + n || b.length < j + n) + return false; + while (--n >= 0) + if (a[i++] != b[j++]) + return false; + return true; + } + + public void write(DataOutputStream writer, int i, int len) throws IOException { + writer.write(m_bytes, m_offset + i, len); + } + + public LuaValue len() { + return LuaInteger.valueOf(m_length); + } + + public int length() { + return m_length; + } + + public int rawlen() { + return m_length; + } + + public int luaByte(int index) { + return m_bytes[m_offset + index] & 0x0FF; + } + + public int charAt(int index) { + if (index < 0 || index >= m_length) + throw new IndexOutOfBoundsException(); + return luaByte(index); + } + + public String checkjstring() { + return tojstring(); + } + + public LuaString checkstring() { + return this; + } + + /** + * Convert value to an input stream. + * + * @return {@link InputStream} whose data matches the bytes in this {@link LuaString} + */ + public InputStream toInputStream() { + return new ByteArrayInputStream(m_bytes, m_offset, m_length); + } + + /** + * Copy the bytes of the string into the given byte array. + * + * @param strOffset offset from which to copy + * @param bytes destination byte array + * @param arrayOffset offset in destination + * @param len number of bytes to copy + */ + public void copyInto(int strOffset, byte[] bytes, int arrayOffset, int len) { + System.arraycopy(m_bytes, m_offset + strOffset, bytes, arrayOffset, len); + } + + /** + * Java version of strpbrk - find index of any byte that in an accept string. + * + * @param accept {@link LuaString} containing characters to look for. + * @return index of first match in the {@code accept} string, or -1 if not found. + */ + public int indexOfAny(LuaString accept) { + final int ilimit = m_offset + m_length; + final int jlimit = accept.m_offset + accept.m_length; + for (int i = m_offset; i < ilimit; ++i) { + for (int j = accept.m_offset; j < jlimit; ++j) { + if (m_bytes[i] == accept.m_bytes[j]) { + return i - m_offset; + } + } + } + return -1; + } + + /** + * Find the index of a byte starting at a point in this string + * + * @param b the byte to look for + * @param start the first index in the string + * @return index of first match found, or -1 if not found. + */ + public int indexOf(byte b, int start) { + for (int i = start; i < m_length; ++i) { + if (m_bytes[m_offset + i] == b) + return i; + } + return -1; + } + + /** + * Find the index of a string starting at a point in this string + * + * @param s the string to search for + * @param start the first index in the string + * @return index of first match found, or -1 if not found. + */ + public int indexOf(LuaString s, int start) { + final int slen = s.length(); + final int limit = m_length - slen; + for (int i = start; i <= limit; ++i) { + if (equals(m_bytes, m_offset + i, s.m_bytes, s.m_offset, slen)) + return i; + } + return -1; + } + + /** + * Find the last index of a string in this string + * + * @param s the string to search for + * @return index of last match found, or -1 if not found. + */ + public int lastIndexOf(LuaString s) { + final int slen = s.length(); + final int limit = m_length - slen; + for (int i = limit; i >= 0; --i) { + if (equals(m_bytes, m_offset + i, s.m_bytes, s.m_offset, slen)) + return i; + } + return -1; + } + + + /** + * Convert to Java String interpreting as utf8 characters. + * + * @param bytes byte array in UTF8 encoding to convert + * @param offset starting index in byte array + * @param length number of bytes to convert + * @return Java String corresponding to the value of bytes interpreted using UTF8 + * @see #lengthAsUtf8(char[]) + * @see #encodeToUtf8(char[], int, byte[], int) + * @see #isValidUtf8() + */ + public static String decodeAsUtf8(byte[] bytes, int offset, int length) { + // planetiler change: use java built-in UTF-8 decoding, which is faster + return new String(bytes, offset, length, StandardCharsets.UTF_8); + } + + /** + * Count the number of bytes required to encode the string as UTF-8. + * + * @param chars Array of unicode characters to be encoded as UTF-8 + * @return count of bytes needed to encode using UTF-8 + * @see #encodeToUtf8(char[], int, byte[], int) + * @see #decodeAsUtf8(byte[], int, int) + * @see #isValidUtf8() + */ + public static int lengthAsUtf8(char[] chars) { + int i, b; + char c; + for (i = b = chars.length; --i >= 0;) + if ((c = chars[i]) >= 0x80) + b += (c >= 0x800) ? 2 : 1; + return b; + } + + /** + * Encode the given Java string as UTF-8 bytes, writing the result to bytes starting at offset. + *

+ * The string should be measured first with lengthAsUtf8 to make sure the given byte array is large enough. + * + * @param chars Array of unicode characters to be encoded as UTF-8 + * @param nchars Number of characters in the array to convert. + * @param bytes byte array to hold the result + * @param off offset into the byte array to start writing + * @return number of bytes converted. + * @see #lengthAsUtf8(char[]) + * @see #decodeAsUtf8(byte[], int, int) + * @see #isValidUtf8() + */ + public static int encodeToUtf8(char[] chars, int nchars, byte[] bytes, int off) { + char c; + int j = off; + for (int i = 0; i < nchars; i++) { + if ((c = chars[i]) < 0x80) { + bytes[j++] = (byte) c; + } else if (c < 0x800) { + bytes[j++] = (byte) (0xC0 | ((c >> 6) & 0x1f)); + bytes[j++] = (byte) (0x80 | (c & 0x3f)); + } else { + bytes[j++] = (byte) (0xE0 | ((c >> 12) & 0x0f)); + bytes[j++] = (byte) (0x80 | ((c >> 6) & 0x3f)); + bytes[j++] = (byte) (0x80 | (c & 0x3f)); + } + } + return j - off; + } + + /** + * Check that a byte sequence is valid UTF-8 + * + * @return true if it is valid UTF-8, otherwise false + * @see #lengthAsUtf8(char[]) + * @see #encodeToUtf8(char[], int, byte[], int) + * @see #decodeAsUtf8(byte[], int, int) + */ + public boolean isValidUtf8() { + + // planetiler change: cache the result of this + if (cached_utf8 == null) { + synchronized (this) { + if (cached_utf8 == null) { + cached_utf8 = computeIsValidUtf8(); + } + } + } + return cached_utf8; + } + + private boolean computeIsValidUtf8() { + for (int i = m_offset, j = m_offset + m_length; i < j;) { + int c = m_bytes[i++]; + if (c >= 0) + continue; + if (((c & 0xE0) == 0xC0) && i < j && (m_bytes[i++] & 0xC0) == 0x80) + continue; + if (((c & 0xF0) == 0xE0) && i + 1 < j && (m_bytes[i++] & 0xC0) == 0x80 && (m_bytes[i++] & 0xC0) == 0x80) + continue; + return false; + } + return true; + } + + // --------------------- number conversion ----------------------- + + /** + * convert to a number using baee 10 or base 16 if it starts with '0x', or NIL if it can't be converted + * + * @return IntValue, DoubleValue, or NIL depending on the content of the string. + * @see LuaValue#tonumber() + */ + public LuaValue tonumber() { + double d = scannumber(); + return Double.isNaN(d) ? NIL : valueOf(d); + } + + /** + * convert to a number using a supplied base, or NIL if it can't be converted + * + * @param base the base to use, such as 10 + * @return IntValue, DoubleValue, or NIL depending on the content of the string. + * @see LuaValue#tonumber() + */ + public LuaValue tonumber(int base) { + double d = scannumber(base); + return Double.isNaN(d) ? NIL : valueOf(d); + } + + /** + * Convert to a number in base 10, or base 16 if the string starts with '0x', or return Double.NaN if it cannot be + * converted to a number. + * + * @return double value if conversion is valid, or Double.NaN if not + */ + public double scannumber() { + int i = m_offset, j = m_offset + m_length; + while (i < j && m_bytes[i] == ' ') + ++i; + while (i < j && m_bytes[j - 1] == ' ') + --j; + if (i >= j) + return Double.NaN; + if (m_bytes[i] == '0' && i + 1 < j && (m_bytes[i + 1] == 'x' || m_bytes[i + 1] == 'X')) + return scanlong(16, i + 2, j); + double l = scanlong(10, i, j); + return Double.isNaN(l) ? scandouble(i, j) : l; + } + + /** + * Convert to a number in a base, or return Double.NaN if not a number. + * + * @param base the base to use between 2 and 36 + * @return double value if conversion is valid, or Double.NaN if not + */ + public double scannumber(int base) { + if (base < 2 || base > 36) + return Double.NaN; + int i = m_offset, j = m_offset + m_length; + while (i < j && m_bytes[i] == ' ') + ++i; + while (i < j && m_bytes[j - 1] == ' ') + --j; + if (i >= j) + return Double.NaN; + return scanlong(base, i, j); + } + + /** + * Scan and convert a long value, or return Double.NaN if not found. + * + * @param base the base to use, such as 10 + * @param start the index to start searching from + * @param end the first index beyond the search range + * @return double value if conversion is valid, or Double.NaN if not + */ + private double scanlong(int base, int start, int end) { + long x = 0; + boolean neg = (m_bytes[start] == '-'); + for (int i = (neg ? start + 1 : start); i < end; i++) { + int digit = m_bytes[i] - (base <= 10 || (m_bytes[i] >= '0' && m_bytes[i] <= '9') ? '0' : + m_bytes[i] >= 'A' && m_bytes[i] <= 'Z' ? ('A' - 10) : ('a' - 10)); + if (digit < 0 || digit >= base) + return Double.NaN; + x = x * base + digit; + if (x < 0) + return Double.NaN; // overflow + } + return neg ? -x : x; + } + + /** + * Scan and convert a double value, or return Double.NaN if not a double. + * + * @param start the index to start searching from + * @param end the first index beyond the search range + * @return double value if conversion is valid, or Double.NaN if not + */ + private double scandouble(int start, int end) { + if (end > start + 64) + end = start + 64; + for (int i = start; i < end; i++) { + switch (m_bytes[i]) { + case '-': + case '+': + case '.': + case 'e': + case 'E': + case '0': + case '1': + case '2': + case '3': + case '4': + case '5': + case '6': + case '7': + case '8': + case '9': + break; + default: + return Double.NaN; + } + } + char[] c = new char[end - start]; + for (int i = start; i < end; i++) + c[i - start] = (char) m_bytes[i]; + try { + return Double.parseDouble(new String(c)); + } catch (Exception e) { + return Double.NaN; + } + } + + /** + * Print the bytes of the LuaString to a PrintStream as if it were an ASCII string, quoting and escaping control + * characters. + * + * @param ps PrintStream to print to. + */ + public void printToStream(PrintStream ps) { + for (int i = 0, n = m_length; i < n; i++) { + int c = m_bytes[m_offset + i]; + ps.print((char) c); + } + } +} diff --git a/planetiler-experimental/src/main/java/org/luaj/vm2/LuaTables.java b/planetiler-experimental/src/main/java/org/luaj/vm2/LuaTables.java new file mode 100644 index 0000000000..086952b077 --- /dev/null +++ b/planetiler-experimental/src/main/java/org/luaj/vm2/LuaTables.java @@ -0,0 +1,11 @@ +package org.luaj.vm2; + +public class LuaTables { + + /** + * Returns true if the lua table is a list, and not a map. + */ + public static boolean isArray(LuaValue v) { + return v instanceof LuaTable table && table.getArrayLength() > 0; + } +} diff --git a/planetiler-experimental/src/main/java/org/luaj/vm2/lib/jse/CoerceJavaToLua.java b/planetiler-experimental/src/main/java/org/luaj/vm2/lib/jse/CoerceJavaToLua.java new file mode 100644 index 0000000000..41c97cde1d --- /dev/null +++ b/planetiler-experimental/src/main/java/org/luaj/vm2/lib/jse/CoerceJavaToLua.java @@ -0,0 +1,195 @@ +/******************************************************************************* + * Copyright (c) 2009-2011 Luaj.org. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + ******************************************************************************/ +package org.luaj.vm2.lib.jse; + +import static java.util.Map.entry; + +import java.util.Map; +import org.luaj.vm2.LuaDouble; +import org.luaj.vm2.LuaInteger; +import org.luaj.vm2.LuaString; +import org.luaj.vm2.LuaUserdata; +import org.luaj.vm2.LuaValue; + +/** + * Modified version of {@link CoerceJavaToLua} that fixes a thread safety issue around concurrent map updates. + *

+ * Helper class to coerce values from Java to lua within the luajava library. + *

+ * This class is primarily used by the {@link org.luaj.vm2.lib.jse.LuajavaLib}, but can also be used directly when + * working with Java/lua bindings. + *

+ * To coerce scalar types, the various, generally the {@code valueOf(type)} methods on {@link LuaValue} may be used: + *

    + *
  • {@link LuaValue#valueOf(boolean)}
  • + *
  • {@link LuaValue#valueOf(byte[])}
  • + *
  • {@link LuaValue#valueOf(double)}
  • + *
  • {@link LuaValue#valueOf(int)}
  • + *
  • {@link LuaValue#valueOf(String)}
  • + *
+ *

+ * To coerce arrays of objects and lists, the {@code listOf(..)} and {@code tableOf(...)} methods on {@link LuaValue} + * may be used: + *

    + *
  • {@link LuaValue#listOf(LuaValue[])}
  • + *
  • {@link LuaValue#listOf(LuaValue[], org.luaj.vm2.Varargs)}
  • + *
  • {@link LuaValue#tableOf(LuaValue[])}
  • + *
  • {@link LuaValue#tableOf(LuaValue[], LuaValue[], org.luaj.vm2.Varargs)}
  • + *
+ * The method {@link CoerceJavaToLua#coerce(Object)} looks as the type and dimesioning of the argument and tries to + * guess the best fit for corrsponding lua scalar, table, or table of tables. + * + * @see CoerceJavaToLua#coerce(Object) + * @see org.luaj.vm2.lib.jse.LuajavaLib + */ +public class CoerceJavaToLua { + + interface Coercion { + LuaValue coerce(Object javaValue); + }; + + private static final class BoolCoercion implements Coercion { + public LuaValue coerce(Object javaValue) { + Boolean b = (Boolean) javaValue; + return b ? LuaValue.TRUE : LuaValue.FALSE; + } + } + + private static final class IntCoercion implements Coercion { + public LuaValue coerce(Object javaValue) { + Number n = (Number) javaValue; + return LuaInteger.valueOf(n.intValue()); + } + } + + private static final class CharCoercion implements Coercion { + public LuaValue coerce(Object javaValue) { + Character c = (Character) javaValue; + return LuaInteger.valueOf(c.charValue()); + } + } + + private static final class DoubleCoercion implements Coercion { + public LuaValue coerce(Object javaValue) { + Number n = (Number) javaValue; + return LuaDouble.valueOf(n.doubleValue()); + } + } + + private static final class StringCoercion implements Coercion { + public LuaValue coerce(Object javaValue) { + return LuaString.valueOf(javaValue.toString()); + } + } + + private static final class BytesCoercion implements Coercion { + public LuaValue coerce(Object javaValue) { + return LuaValue.valueOf((byte[]) javaValue); + } + } + + private static final class ClassCoercion implements Coercion { + public LuaValue coerce(Object javaValue) { + return JavaClass.forClass((Class) javaValue); + } + } + + private static final class InstanceCoercion implements Coercion { + public LuaValue coerce(Object javaValue) { + return new JavaInstance(javaValue); + } + } + + private static final class ArrayCoercion implements Coercion { + public LuaValue coerce(Object javaValue) { + // should be userdata? + return new JavaArray(javaValue); + } + } + + private static final class LuaCoercion implements Coercion { + public LuaValue coerce(Object javaValue) { + return (LuaValue) javaValue; + } + } + + + // planetiler change: use immutable thread-safe map + private static final Map, Coercion> COERCIONS; + + static { + Coercion boolCoercion = new BoolCoercion(); + Coercion intCoercion = new IntCoercion(); + Coercion charCoercion = new CharCoercion(); + Coercion doubleCoercion = new DoubleCoercion(); + Coercion stringCoercion = new StringCoercion(); + Coercion bytesCoercion = new BytesCoercion(); + Coercion classCoercion = new ClassCoercion(); + COERCIONS = Map.ofEntries( + entry(Boolean.class, boolCoercion), + entry(Byte.class, intCoercion), + entry(Character.class, charCoercion), + entry(Short.class, intCoercion), + entry(Integer.class, intCoercion), + entry(Long.class, doubleCoercion), + entry(Float.class, doubleCoercion), + entry(Double.class, doubleCoercion), + entry(String.class, stringCoercion), + entry(byte[].class, bytesCoercion), + entry(Class.class, classCoercion) + ); + } + + /** + * Coerse a Java object to a corresponding lua value. + *

+ * Integral types {@code boolean}, {@code byte}, {@code char}, and {@code int} will become {@link LuaInteger}; + * {@code long}, {@code float}, and {@code double} will become {@link LuaDouble}; {@code String} and {@code byte[]} + * will become {@link LuaString}; types inheriting from {@link LuaValue} will be returned without coercion; other + * types will become {@link LuaUserdata}. + * + * @param o Java object needing conversion + * @return {@link LuaValue} corresponding to the supplied Java value. + * @see LuaValue + * @see LuaInteger + * @see LuaDouble + * @see LuaString + * @see LuaUserdata + */ + public static LuaValue coerce(Object o) { + if (o == null) + return LuaValue.NIL; + Class clazz = o.getClass(); + // planetiler change: don't modify coercions + Coercion c = COERCIONS.get(clazz); + if (c == null) { + c = o instanceof LuaValue ? luaCoercion : clazz.isArray() ? arrayCoercion : instanceCoercion; + } + return c.coerce(o); + } + + static final Coercion instanceCoercion = new InstanceCoercion(); + + static final Coercion arrayCoercion = new ArrayCoercion(); + + static final Coercion luaCoercion = new LuaCoercion(); +} diff --git a/planetiler-experimental/src/main/java/org/luaj/vm2/lib/jse/CoerceLuaToJava.java b/planetiler-experimental/src/main/java/org/luaj/vm2/lib/jse/CoerceLuaToJava.java new file mode 100644 index 0000000000..2998129a34 --- /dev/null +++ b/planetiler-experimental/src/main/java/org/luaj/vm2/lib/jse/CoerceLuaToJava.java @@ -0,0 +1,434 @@ +/******************************************************************************* + * Copyright (c) 2009-2011 Luaj.org. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + ******************************************************************************/ +package org.luaj.vm2.lib.jse; + +import java.lang.reflect.Array; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import org.luaj.vm2.LuaInteger; +import org.luaj.vm2.LuaString; +import org.luaj.vm2.LuaTable; +import org.luaj.vm2.LuaValue; + +/** + * Modified version of {@link CoerceLuaToJava} that fixes a thread safety issue around concurrent map updates and also + * removes usage of deprecated value class constructors. + *

+ * Helper class to coerce values from lua to Java within the luajava library. + *

+ * This class is primarily used by the {@link org.luaj.vm2.lib.jse.LuajavaLib}, but can also be used directly when + * working with Java/lua bindings. + *

+ * To coerce to specific Java values, generally the {@code toType()} methods on {@link LuaValue} may be used: + *

    + *
  • {@link LuaValue#toboolean()}
  • + *
  • {@link LuaValue#tobyte()}
  • + *
  • {@link LuaValue#tochar()}
  • + *
  • {@link LuaValue#toshort()}
  • + *
  • {@link LuaValue#toint()}
  • + *
  • {@link LuaValue#tofloat()}
  • + *
  • {@link LuaValue#todouble()}
  • + *
  • {@link LuaValue#tojstring()}
  • + *
  • {@link LuaValue#touserdata()}
  • + *
  • {@link LuaValue#touserdata(Class)}
  • + *
+ *

+ * For data in lua tables, the various methods on {@link LuaTable} can be used directly to convert data to something + * more useful. + * + * @see org.luaj.vm2.lib.jse.LuajavaLib + * @see CoerceJavaToLua + */ +public class CoerceLuaToJava { + + static final int SCORE_NULL_VALUE = 0x10; + static final int SCORE_WRONG_TYPE = 0x100; + static final int SCORE_UNCOERCIBLE = 0x10000; + + interface Coercion { + int score(LuaValue value); + + Object coerce(LuaValue value); + } + + /** + * Coerce a LuaValue value to a specified java class + * + * @param value LuaValue to coerce + * @param clazz Class to coerce into + * @return Object of type clazz (or a subclass) with the corresponding value. + */ + public static Object coerce(LuaValue value, Class clazz) { + return getCoercion(clazz).coerce(value); + } + + static final Map, Coercion> COERCIONS = new ConcurrentHashMap<>(); + + static final class BoolCoercion implements Coercion { + public String toString() { + return "BoolCoercion()"; + } + + public int score(LuaValue value) { + return value.type() == LuaValue.TBOOLEAN ? 0 : 1; + } + + public Object coerce(LuaValue value) { + return value.toboolean() ? Boolean.TRUE : Boolean.FALSE; + } + } + + static final class NumericCoercion implements Coercion { + static final int TARGET_TYPE_BYTE = 0; + static final int TARGET_TYPE_CHAR = 1; + static final int TARGET_TYPE_SHORT = 2; + static final int TARGET_TYPE_INT = 3; + static final int TARGET_TYPE_LONG = 4; + static final int TARGET_TYPE_FLOAT = 5; + static final int TARGET_TYPE_DOUBLE = 6; + static final String[] TYPE_NAMES = {"byte", "char", "short", "int", "long", "float", "double"}; + final int targetType; + + public String toString() { + return "NumericCoercion(" + TYPE_NAMES[targetType] + ")"; + } + + NumericCoercion(int targetType) { + this.targetType = targetType; + } + + public int score(LuaValue value) { + int fromStringPenalty = 0; + if (value.type() == LuaValue.TSTRING) { + value = value.tonumber(); + if (value.isnil()) { + return SCORE_UNCOERCIBLE; + } + fromStringPenalty = 4; + } + if (value.isint()) { + switch (targetType) { + case TARGET_TYPE_BYTE: { + int i = value.toint(); + return fromStringPenalty + ((i == (byte) i) ? 0 : SCORE_WRONG_TYPE); + } + case TARGET_TYPE_CHAR: { + int i = value.toint(); + return fromStringPenalty + ((i == (byte) i) ? 1 : (i == (char) i) ? 0 : SCORE_WRONG_TYPE); + } + case TARGET_TYPE_SHORT: { + int i = value.toint(); + return fromStringPenalty + + ((i == (byte) i) ? 1 : (i == (short) i) ? 0 : SCORE_WRONG_TYPE); + } + case TARGET_TYPE_INT: { + int i = value.toint(); + return fromStringPenalty + + ((i == (byte) i) ? 2 : ((i == (char) i) || (i == (short) i)) ? 1 : 0); + } + case TARGET_TYPE_FLOAT: + return fromStringPenalty + 1; + case TARGET_TYPE_LONG: + return fromStringPenalty + 1; + case TARGET_TYPE_DOUBLE: + return fromStringPenalty + 2; + default: + return SCORE_WRONG_TYPE; + } + } else if (value.isnumber()) { + switch (targetType) { + case TARGET_TYPE_BYTE: + return SCORE_WRONG_TYPE; + case TARGET_TYPE_CHAR: + return SCORE_WRONG_TYPE; + case TARGET_TYPE_SHORT: + return SCORE_WRONG_TYPE; + case TARGET_TYPE_INT: + return SCORE_WRONG_TYPE; + case TARGET_TYPE_LONG: { + double d = value.todouble(); + return fromStringPenalty + ((d == (long) d) ? 0 : SCORE_WRONG_TYPE); + } + case TARGET_TYPE_FLOAT: { + double d = value.todouble(); + return fromStringPenalty + ((d == (float) d) ? 0 : SCORE_WRONG_TYPE); + } + case TARGET_TYPE_DOUBLE: { + double d = value.todouble(); + return fromStringPenalty + (((d == (long) d) || (d == (float) d)) ? 1 : 0); + } + default: + return SCORE_WRONG_TYPE; + } + } else { + return SCORE_UNCOERCIBLE; + } + } + + public Object coerce(LuaValue value) { + // planetiler change: don't use deprecated value class constructors + return switch (targetType) { + case TARGET_TYPE_BYTE -> (byte) value.toint(); + case TARGET_TYPE_CHAR -> (char) value.toint(); + case TARGET_TYPE_SHORT -> (short) value.toint(); + case TARGET_TYPE_INT -> value.toint(); + case TARGET_TYPE_LONG -> (long) value.todouble(); + case TARGET_TYPE_FLOAT -> (float) value.todouble(); + case TARGET_TYPE_DOUBLE -> value.todouble(); + default -> null; + }; + } + } + + static final class StringCoercion implements Coercion { + public static final int TARGET_TYPE_STRING = 0; + public static final int TARGET_TYPE_BYTES = 1; + final int targetType; + + public StringCoercion(int targetType) { + this.targetType = targetType; + } + + public String toString() { + return "StringCoercion(" + (targetType == TARGET_TYPE_STRING ? "String" : "byte[]") + ")"; + } + + public int score(LuaValue value) { + switch (value.type()) { + case LuaValue.TSTRING: + return value.checkstring().isValidUtf8() ? + (targetType == TARGET_TYPE_STRING ? 0 : 1) : + (targetType == TARGET_TYPE_BYTES ? 0 : SCORE_WRONG_TYPE); + case LuaValue.TNIL: + return SCORE_NULL_VALUE; + default: + return targetType == TARGET_TYPE_STRING ? SCORE_WRONG_TYPE : SCORE_UNCOERCIBLE; + } + } + + public Object coerce(LuaValue value) { + if (value.isnil()) + return null; + if (targetType == TARGET_TYPE_STRING) + return value.tojstring(); + LuaString s = value.checkstring(); + byte[] b = new byte[s.m_length]; + s.copyInto(0, b, 0, b.length); + return b; + } + } + + static final class ArrayCoercion implements Coercion { + final Class componentType; + final Coercion componentCoercion; + + public ArrayCoercion(Class componentType) { + this.componentType = componentType; + this.componentCoercion = getCoercion(componentType); + } + + public String toString() { + return "ArrayCoercion(" + componentType.getName() + ")"; + } + + public int score(LuaValue value) { + switch (value.type()) { + case LuaValue.TTABLE: + return value.length() == 0 ? 0 : componentCoercion.score(value.get(1)); + case LuaValue.TUSERDATA: + return inheritanceLevels(componentType, value.touserdata().getClass().getComponentType()); + case LuaValue.TNIL: + return SCORE_NULL_VALUE; + default: + return SCORE_UNCOERCIBLE; + } + } + + public Object coerce(LuaValue value) { + switch (value.type()) { + case LuaValue.TTABLE: { + int n = value.length(); + Object a = Array.newInstance(componentType, n); + for (int i = 0; i < n; i++) + Array.set(a, i, componentCoercion.coerce(value.get(i + 1))); + return a; + } + case LuaValue.TUSERDATA: + return value.touserdata(); + case LuaValue.TNIL: + return null; + default: + return null; + } + + } + } + + static final class EnumCoercion implements Coercion { + private final Map lookup = new HashMap<>(); + private final Class enumType; + + + public EnumCoercion(Class enumType) { + this.enumType = enumType; + for (Object e : enumType.getEnumConstants()) { + lookup.put(LuaString.valueOf(e.toString()), e); + lookup.put(LuaInteger.valueOf(((Enum) e).ordinal()), e); + } + } + + public String toString() { + return "EnumCoercion(" + enumType.getName() + ")"; + } + + public int score(LuaValue value) { + return switch (value.type()) { + case LuaValue.TNUMBER, LuaValue.TSTRING -> 0; + case LuaValue.TUSERDATA -> value.touserdata().getClass() == enumType ? 0 : SCORE_UNCOERCIBLE; + default -> SCORE_UNCOERCIBLE; + }; + } + + public Object coerce(LuaValue value) { + return switch (value.type()) { + case LuaValue.TNUMBER, LuaValue.TSTRING -> lookup.get(value); + case LuaValue.TUSERDATA -> value.touserdata(); + default -> null; + }; + } + } + + private static final Map inheritanceLevelsCache = new ConcurrentHashMap<>(); + + private record ClassPair(Class baseClass, Class subclass) {} + + /** + * Determine levels of inheritance between a base class and a subclass + * + * @param baseclass base class to look for + * @param subclass class from which to start looking + * @return number of inheritance levels between subclass and baseclass, or SCORE_UNCOERCIBLE if not a subclass + */ + static int inheritanceLevels(Class baseclass, Class subclass) { + if (subclass == null) + return SCORE_UNCOERCIBLE; + if (baseclass == subclass) + return 0; + // planetiler change: cache result of this value to improve performance + ClassPair key = new ClassPair(baseclass, subclass); + Integer result = inheritanceLevelsCache.get(key); + if (result != null) { + return result; + } + int min = Math.min(SCORE_UNCOERCIBLE, inheritanceLevels(baseclass, subclass.getSuperclass()) + 1); + Class[] ifaces = subclass.getInterfaces(); + for (Class iface : ifaces) + min = Math.min(min, inheritanceLevels(baseclass, iface) + 1); + // best-effort cache, might end up computing a few times but that's ok + inheritanceLevelsCache.put(key, min); + return min; + } + + static final class ObjectCoercion implements Coercion { + final Class targetType; + + ObjectCoercion(Class targetType) { + this.targetType = targetType; + } + + public String toString() { + return "ObjectCoercion(" + targetType.getName() + ")"; + } + + public int score(LuaValue value) { + return switch (value.type()) { + case LuaValue.TNUMBER -> inheritanceLevels(targetType, value.isint() ? Integer.class : Double.class); + case LuaValue.TBOOLEAN -> inheritanceLevels(targetType, Boolean.class); + case LuaValue.TSTRING -> inheritanceLevels(targetType, String.class); + case LuaValue.TUSERDATA -> inheritanceLevels(targetType, value.touserdata().getClass()); + case LuaValue.TNIL -> SCORE_NULL_VALUE; + default -> inheritanceLevels(targetType, value.getClass()); + }; + } + + public Object coerce(LuaValue value) { + // planetiler change: don't use deprecated value class constructors + return switch (value.type()) { + case LuaValue.TNUMBER -> value.isint() ? (Object) value.toint() : (Object) value.todouble(); + case LuaValue.TBOOLEAN -> value.toboolean() ? Boolean.TRUE : Boolean.FALSE; + case LuaValue.TSTRING -> value.tojstring(); + case LuaValue.TUSERDATA -> value.optuserdata(targetType, null); + case LuaValue.TNIL -> null; + default -> value; + }; + } + } + + static { + Coercion boolCoercion = new BoolCoercion(); + Coercion byteCoercion = new NumericCoercion(NumericCoercion.TARGET_TYPE_BYTE); + Coercion charCoercion = new NumericCoercion(NumericCoercion.TARGET_TYPE_CHAR); + Coercion shortCoercion = new NumericCoercion(NumericCoercion.TARGET_TYPE_SHORT); + Coercion intCoercion = new NumericCoercion(NumericCoercion.TARGET_TYPE_INT); + Coercion longCoercion = new NumericCoercion(NumericCoercion.TARGET_TYPE_LONG); + Coercion floatCoercion = new NumericCoercion(NumericCoercion.TARGET_TYPE_FLOAT); + Coercion doubleCoercion = new NumericCoercion(NumericCoercion.TARGET_TYPE_DOUBLE); + Coercion stringCoercion = new StringCoercion(StringCoercion.TARGET_TYPE_STRING); + Coercion bytesCoercion = new StringCoercion(StringCoercion.TARGET_TYPE_BYTES); + + COERCIONS.put(Boolean.TYPE, boolCoercion); + COERCIONS.put(Boolean.class, boolCoercion); + COERCIONS.put(Byte.TYPE, byteCoercion); + COERCIONS.put(Byte.class, byteCoercion); + COERCIONS.put(Character.TYPE, charCoercion); + COERCIONS.put(Character.class, charCoercion); + COERCIONS.put(Short.TYPE, shortCoercion); + COERCIONS.put(Short.class, shortCoercion); + COERCIONS.put(Integer.TYPE, intCoercion); + COERCIONS.put(Integer.class, intCoercion); + COERCIONS.put(Long.TYPE, longCoercion); + COERCIONS.put(Long.class, longCoercion); + COERCIONS.put(Float.TYPE, floatCoercion); + COERCIONS.put(Float.class, floatCoercion); + COERCIONS.put(Double.TYPE, doubleCoercion); + COERCIONS.put(Double.class, doubleCoercion); + COERCIONS.put(String.class, stringCoercion); + COERCIONS.put(byte[].class, bytesCoercion); + } + + static Coercion getCoercion(Class c) { + Coercion co = COERCIONS.get(c); + if (co != null) { + return co; + } + if (c.isArray()) { + co = new ArrayCoercion(c.getComponentType()); + } else if (c.isEnum()) { + co = new EnumCoercion(c); + } else { + co = new ObjectCoercion(c); + } + COERCIONS.putIfAbsent(c, co); + return co; + } +} diff --git a/planetiler-experimental/src/main/java/org/luaj/vm2/lib/jse/ExtraPlanetilerCoercions.java b/planetiler-experimental/src/main/java/org/luaj/vm2/lib/jse/ExtraPlanetilerCoercions.java new file mode 100644 index 0000000000..ff02249f5f --- /dev/null +++ b/planetiler-experimental/src/main/java/org/luaj/vm2/lib/jse/ExtraPlanetilerCoercions.java @@ -0,0 +1,74 @@ +package org.luaj.vm2.lib.jse; + +import static org.luaj.vm2.lib.jse.CoerceLuaToJava.SCORE_WRONG_TYPE; + +import com.onthegomap.planetiler.experimental.lua.LuaConversions; +import java.nio.file.Path; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.function.Function; +import java.util.function.ToIntFunction; +import org.luaj.vm2.LuaTables; +import org.luaj.vm2.LuaValue; + +/** + * Call {@link #install()} to install planetiler's extra type conversions: {@link List}/table + */ +public class ExtraPlanetilerCoercions { + public static void install() {} + + static { + coerceTable(List.class, LuaConversions::toJavaList); + coerceTable(Collection.class, LuaConversions::toJavaCollection); + coerceTable(Iterable.class, LuaConversions::toJavaIterable); + coerceTable(Set.class, LuaConversions::toJavaSet); + coerceTable(Map.class, LuaConversions::toJavaMap); + CoerceLuaToJava.COERCIONS.put(Path.class, new PathCoercion()); + } + + private static void coerceTable(Class clazz, Function coerce) { + coerce(clazz, value -> value.type() == LuaValue.TTABLE ? 0 : SCORE_WRONG_TYPE, coerce); + } + + private static void coerce(Class clazz, ToIntFunction score, + Function coerce) { + CoerceLuaToJava.COERCIONS.put(clazz, new ContainerCoercion(score, coerce)); + } + + record ContainerCoercion(ToIntFunction score, Function coerce) + implements CoerceLuaToJava.Coercion { + + @Override + public int score(LuaValue value) { + return score.applyAsInt(value); + } + + @Override + public Object coerce(LuaValue value) { + return coerce.apply(value); + } + } + + private static class PathCoercion implements CoerceLuaToJava.Coercion { + @Override + public int score(LuaValue value) { + return value.isstring() || LuaTables.isArray(value) ? 0 : SCORE_WRONG_TYPE; + } + + @Override + public Object coerce(LuaValue value) { + if (value.isstring()) { + return Path.of(value.tojstring()); + } + int len = value.length(); + String main = value.get(1).tojstring(); + String[] next = new String[len - 1]; + for (int i = 1; i < len; i++) { + next[i - 1] = value.get(i + 1).tojstring(); + } + return Path.of(main, next); + } + } +} diff --git a/planetiler-experimental/src/main/java/org/luaj/vm2/lib/jse/JavaClass.java b/planetiler-experimental/src/main/java/org/luaj/vm2/lib/jse/JavaClass.java new file mode 100644 index 0000000000..d3eeac8f1d --- /dev/null +++ b/planetiler-experimental/src/main/java/org/luaj/vm2/lib/jse/JavaClass.java @@ -0,0 +1,219 @@ +/******************************************************************************* + * Copyright (c) 2011 Luaj.org. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + ******************************************************************************/ +package org.luaj.vm2.lib.jse; + +import com.onthegomap.planetiler.experimental.lua.JavaToLuaCase; +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; +import java.lang.reflect.InaccessibleObjectException; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.stream.Collectors; +import org.luaj.vm2.LuaString; +import org.luaj.vm2.LuaValue; + +/** + * Modified version of {@link org.luaj.vm2.lib.jse.JavaClass} that uses {@link ConcurrentHashMap} instead of a + * synchronized map to cache classes to improve performance, and also adds some utilities to improve java interop. + *

+ * LuaValue that represents a Java class. + *

+ * Will respond to get() and set() by returning field values, or java methods. + *

+ * This class is not used directly. It is returned by calls to {@link CoerceJavaToLua#coerce(Object)} when a Class is + * supplied. + * + * @see CoerceJavaToLua + * @see CoerceLuaToJava + */ +public class JavaClass extends JavaInstance { + + static final Map, JavaClass> classes = new ConcurrentHashMap<>(); + + static final LuaValue NEW = valueOf("new"); + + private final Map fields = new HashMap<>(); + private final Map methods = new HashMap<>(); + private final Map getters = new HashMap<>(); + private final Map setters = new HashMap<>(); + private final Map> innerclasses = new HashMap<>(); + public final boolean bindMethods; + + public static JavaClass forClass(Class c) { + // planetiler change: use ConcurrentHashMap instead of synchronized map to improve performance + JavaClass j = classes.get(c); + if (j == null) { + j = classes.computeIfAbsent(c, JavaClass::new); + } + return j; + } + + JavaClass(Class c) { + super(c); + this.bindMethods = c.isAnnotationPresent(LuaBindMethods.class); + // planetiler change: compute these maps eagerly + computeFields(); + computeMethods(); + computeInnerClasses(); + } + + private Map computeFields() { + Field[] f = ((Class) m_instance).getFields(); + for (Field fi : f) { + if (Modifier.isPublic(fi.getModifiers())) { + fields.put(LuaValue.valueOf(fi.getName()), fi); + try { + if (!fi.isAccessible()) { + fi.setAccessible(true); + } + } catch (SecurityException | InaccessibleObjectException s) { + } + } + } + + // planetiler change: add snake_case aliases for camelCase methods + putAliases(fields); + return fields; + } + + private void computeMethods() { + Map> namedlists = new HashMap<>(); + Class clazz = (Class) m_instance; + Set recordComponents = + clazz.isRecord() ? Arrays.stream(clazz.getRecordComponents()).map(c -> c.getName()).collect(Collectors.toSet()) : + Set.of(); + for (Method mi : clazz.getMethods()) { + if (Modifier.isPublic(mi.getModifiers())) { + String name = mi.getName(); + // planetiler change: allow methods annotated with @LuaGetter or @LuaSetter to simulate property access + // also allow record components to be accessed as properties + if ((recordComponents.contains(name) || mi.isAnnotationPresent(LuaGetter.class)) && + mi.getParameterCount() == 0) { + getters.put(LuaString.valueOf(name), new Getter(mi)); + } else if (mi.isAnnotationPresent(LuaSetter.class)) { + setters.put(LuaString.valueOf(name), new Setter(mi, mi.getParameterTypes()[0])); + } + namedlists.computeIfAbsent(name, k -> new ArrayList<>()).add(JavaMethod.forMethod(mi)); + } + } + Constructor[] c = ((Class) m_instance).getConstructors(); + List list = new ArrayList<>(); + for (Constructor constructor : c) { + if (Modifier.isPublic(constructor.getModifiers())) { + list.add(JavaConstructor.forConstructor(constructor)); + } + } + switch (list.size()) { + case 0: + break; + case 1: + methods.put(NEW, list.get(0)); + break; + default: + methods.put(NEW, + JavaConstructor.forConstructors(list.toArray(JavaConstructor[]::new))); + break; + } + + for (Entry> e : namedlists.entrySet()) { + String name = e.getKey(); + List classMethods = e.getValue(); + LuaValue luaMethod = classMethods.size() == 1 ? + classMethods.get(0) : + JavaMethod.forMethods(classMethods.toArray(JavaMethod[]::new)); + methods.put(LuaValue.valueOf(name), luaMethod); + } + + // planetiler change: add snake_case aliases for camelCase methods + putAliases(methods); + putAliases(getters); + putAliases(setters); + } + + private void computeInnerClasses() { + Class[] c = ((Class) m_instance).getClasses(); + for (Class ci : c) { + String name = ci.getName(); + String stub = name.substring(Math.max(name.lastIndexOf('$'), name.lastIndexOf('.')) + 1); + innerclasses.put(LuaValue.valueOf(stub), ci); + } + } + + private void putAliases(Map map) { + for (var entry : List.copyOf(map.entrySet())) { + String key = entry.getKey().tojstring(); + String key2 = JavaToLuaCase.transformMemberName(key); + map.putIfAbsent(LuaValue.valueOf(key2), entry.getValue()); + } + } + + Field getField(LuaValue key) { + return fields.get(key); + } + + LuaValue getMethod(LuaValue key) { + return methods.get(key); + } + + Map getMethods() { + return methods; + } + + Class getInnerClass(LuaValue key) { + return innerclasses.get(key); + } + + public LuaValue getConstructor() { + return getMethod(NEW); + } + + public Getter getGetter(LuaValue key) { + return getters.get(key); + } + + public Setter getSetter(LuaValue key) { + return setters.get(key); + } + + public record Getter(Method method) { + + Object get(Object obj) throws InvocationTargetException, IllegalAccessException { + return method.invoke(obj); + } + } + + public record Setter(Method method, Class type) { + + void set(Object obj, Object value) throws InvocationTargetException, IllegalAccessException { + method.invoke(obj, value); + } + } +} diff --git a/planetiler-experimental/src/main/java/org/luaj/vm2/lib/jse/JavaInstance.java b/planetiler-experimental/src/main/java/org/luaj/vm2/lib/jse/JavaInstance.java new file mode 100644 index 0000000000..61697a7853 --- /dev/null +++ b/planetiler-experimental/src/main/java/org/luaj/vm2/lib/jse/JavaInstance.java @@ -0,0 +1,207 @@ +/******************************************************************************* + * Copyright (c) 2011 Luaj.org. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + ******************************************************************************/ +package org.luaj.vm2.lib.jse; + +import static com.onthegomap.planetiler.experimental.lua.LuaConversions.toLuaTable; + +import java.lang.reflect.Field; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import org.luaj.vm2.LuaError; +import org.luaj.vm2.LuaFunction; +import org.luaj.vm2.LuaTable; +import org.luaj.vm2.LuaUserdata; +import org.luaj.vm2.LuaValue; +import org.luaj.vm2.Varargs; + +/** + * Modified version of {@link JavaInstance} with some tweaks to improve java interop. + *

+ * LuaValue that represents a Java instance. + *

+ * Will respond to get() and set() by returning field values or methods. + *

+ * This class is not used directly. It is returned by calls to {@link CoerceJavaToLua#coerce(Object)} when a subclass of + * Object is supplied. + * + * @see CoerceJavaToLua + * @see CoerceLuaToJava + */ +class JavaInstance extends LuaUserdata { + + private final JavaClass jclass; + private final Map boundMethods; + + JavaInstance(Object instance) { + super(instance); + // planetiler change: when class annotated with @LuaBindMethods, allow methods to be called with instance.method() + var clazz = m_instance.getClass(); + jclass = this instanceof JavaClass c ? c : JavaClass.forClass(clazz); + if (clazz.isAnnotationPresent(LuaBindMethods.class)) { + boundMethods = jclass.getMethods().entrySet().stream().collect(Collectors.toMap( + Map.Entry::getKey, + entry -> new BoundMethod(entry.getValue()) + )); + } else { + boundMethods = null; + } + } + + @Override + public LuaTable checktable() { + // planetiler change: allow maps and lists to be accessed as tables + return switch (m_instance) { + case Collection c -> toLuaTable(c); + case Map m -> toLuaTable(m); + default -> super.checktable(); + }; + } + + // planetiler change: allow methods on classes annotated with @LuaBindMethods to be called with instance.method() + private class BoundMethod extends LuaFunction { + + private final LuaValue method; + + BoundMethod(LuaValue method) { + this.method = method; + } + + @Override + public LuaValue call() { + return method.call(JavaInstance.this); + } + + @Override + public LuaValue call(LuaValue arg) { + return method.call(JavaInstance.this, arg); + } + + @Override + public LuaValue call(LuaValue arg1, LuaValue arg2) { + return method.call(JavaInstance.this, arg1, arg2); + } + + @Override + public LuaValue call(LuaValue arg1, LuaValue arg2, LuaValue arg3) { + return method.invoke(LuaValue.varargsOf(new LuaValue[]{JavaInstance.this, arg1, arg2, arg3})).arg(1); + } + + @Override + public Varargs invoke(Varargs args) { + return method.invoke(LuaValue.varargsOf(JavaInstance.this, args)); + } + } + + @Override + public LuaValue len() { + if (m_instance instanceof List c) { + return LuaValue.valueOf(c.size()); + } + return super.len(); + } + + public LuaValue get(LuaValue key) { + // planetiler change: allow lists to be accessed as tables + if (m_instance instanceof List c) { + int idx = key.toint(); + return idx <= 0 || idx > c.size() ? LuaValue.NIL : CoerceJavaToLua.coerce(c.get(idx - 1)); + } + Field f = jclass.getField(key); + if (f != null) + try { + return CoerceJavaToLua.coerce(f.get(m_instance)); + } catch (Exception e) { + throw new LuaError(e); + } + // planetiler change: allow getter methods + var getter = jclass.getGetter(key); + if (getter != null) { + try { + return CoerceJavaToLua.coerce(getter.get(m_instance)); + } catch (Exception e) { + throw new LuaError(e); + } + } + LuaValue m = boundMethods != null ? boundMethods.get(key) : jclass.getMethod(key); + if (m != null) + return m; + Class c = jclass.getInnerClass(key); + if (c != null) + return JavaClass.forClass(c); + + // planetiler change: allow maps to be accessed as tables + if (m_instance instanceof Map map) { + Object key2 = CoerceLuaToJava.coerce(key, Object.class); + if (key2 != null) { + Object value = map.get(key2); + if (value != null) { + return CoerceJavaToLua.coerce(value); + } + } + } + return super.get(key); + } + + public void set(LuaValue key, LuaValue value) { + // planetiler change: allow lists to be accessed as tables + if (key.isnumber() && m_instance instanceof List c) { + c.set(key.toint() - 1, CoerceLuaToJava.coerce(value, Object.class)); + return; + } + Field f = jclass.getField(key); + if (f != null) + try { + f.set(m_instance, CoerceLuaToJava.coerce(value, f.getType())); + return; + } catch (Exception e) { + throw new LuaError(e); + } + // planetiler change: allow setter methods + var setter = jclass.getSetter(key); + if (setter != null) { + try { + setter.set(m_instance, CoerceLuaToJava.coerce(value, setter.type())); + return; + } catch (Exception e) { + throw new LuaError(e); + } + } + + // planetiler change: allow maps to be accessed as tables + if (m_instance instanceof Map map) { + Object key2 = CoerceLuaToJava.coerce(key, Object.class); + if (key2 != null) { + Object value2 = CoerceLuaToJava.coerce(value, Object.class); + if (value2 == null) { + map.remove(key2); + } else { + map.put(key2, value2); + } + return; + } + } + super.set(key, value); + } + +} diff --git a/planetiler-experimental/src/main/java/org/luaj/vm2/lib/jse/JavaMethod.java b/planetiler-experimental/src/main/java/org/luaj/vm2/lib/jse/JavaMethod.java new file mode 100644 index 0000000000..11a01eccfd --- /dev/null +++ b/planetiler-experimental/src/main/java/org/luaj/vm2/lib/jse/JavaMethod.java @@ -0,0 +1,224 @@ +/******************************************************************************* + * Copyright (c) 2011 Luaj.org. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + ******************************************************************************/ +package org.luaj.vm2.lib.jse; + +import java.lang.reflect.InaccessibleObjectException; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import org.luaj.vm2.LuaError; +import org.luaj.vm2.LuaFunction; +import org.luaj.vm2.LuaValue; +import org.luaj.vm2.Varargs; + +/** + * Modified version of {@link JavaMethod} with concurrency fixes and a fix to how varargs are handled. + *

+ * LuaValue that represents a Java method. + *

+ * Can be invoked via call(LuaValue...) and related methods. + *

+ * This class is not used directly. It is returned by calls to calls to {@link JavaInstance#get(LuaValue key)} when a + * method is named. + * + * @see CoerceJavaToLua + * @see CoerceLuaToJava + */ +class JavaMethod extends JavaMember { + + @Override + public String toString() { + return "JavaMethod(" + method + ")"; + } + + // planetiler change: use concurrent hash map instead of synchronized map + static final Map methods = new ConcurrentHashMap<>(); + + static JavaMethod forMethod(Method m) { + JavaMethod j = methods.get(m); + if (j == null) + j = methods.computeIfAbsent(m, JavaMethod::new); + return j; + } + + static LuaFunction forMethods(JavaMethod[] m) { + return new Overload(m); + } + + final Method method; + + JavaMethod(Method m) { + super(m.getParameterTypes(), m.getModifiers()); + this.method = m; + try { + if (!m.isAccessible()) + m.setAccessible(true); + } catch (SecurityException | InaccessibleObjectException s) { + } + } + + public LuaValue call() { + return error("method cannot be called without instance"); + } + + public LuaValue call(LuaValue arg) { + return invokeMethod(arg.checkuserdata(), LuaValue.NONE); + } + + public LuaValue call(LuaValue arg1, LuaValue arg2) { + return invokeMethod(arg1.checkuserdata(), arg2); + } + + public LuaValue call(LuaValue arg1, LuaValue arg2, LuaValue arg3) { + return invokeMethod(arg1.checkuserdata(), LuaValue.varargsOf(arg2, arg3)); + } + + public Varargs invoke(Varargs args) { + return invokeMethod(args.checkuserdata(1), args.subargs(2)); + } + + @Override + protected Object[] convertArgs(Varargs args) { + Object[] a; + if (varargs == null) { + a = new Object[fixedargs.length]; + for (int i = 0; i < a.length; i++) + a[i] = fixedargs[i].coerce(args.arg(i + 1)); + } else { + // planetiler fix: pass last arg through as vararg array parameter + a = new Object[fixedargs.length + 1]; + for (int i = 0; i < fixedargs.length; i++) + a[i] = fixedargs[i].coerce(args.arg(i + 1)); + a[a.length - 1] = varargs.coerce(LuaValue.listOf(null, args.subargs(fixedargs.length + 1))); + } + return a; + } + + @Override + int score(Varargs args) { + int n = args.narg(); + int s = n > fixedargs.length && varargs == null ? CoerceLuaToJava.SCORE_WRONG_TYPE * (n - fixedargs.length) : 0; + for (int j = 0; j < fixedargs.length; j++) + s += fixedargs[j].score(args.arg(j + 1)); + // planetiler fix: use component coercion, not array coercion + if (varargs instanceof CoerceLuaToJava.ArrayCoercion arrayCoercion) + for (int k = fixedargs.length; k < n; k++) + s += arrayCoercion.componentCoercion.score(args.arg(k + 1)); + return s; + } + + LuaValue invokeMethod(Object instance, Varargs args) { + Object[] a = convertArgs(args); + try { + return CoerceJavaToLua.coerce(method.invoke(instance, a)); + } catch (InvocationTargetException e) { + throw new LuaError(e.getTargetException()); + } catch (Exception e) { + return LuaValue.error("coercion error " + e); + } + } + + /** + * LuaValue that represents an overloaded Java method. + *

+ * On invocation, will pick the best method from the list, and invoke it. + *

+ * This class is not used directly. It is returned by calls to calls to {@link JavaInstance#get(LuaValue key)} when an + * overloaded method is named. + */ + static class Overload extends LuaFunction { + + private final JavaMethod[][] methodsByArgCount; + + Overload(JavaMethod[] methods) { + int max = 0; + // planetiler change: precompute which methods are valid based on number of args + for (JavaMethod method : methods) { + max = Math.max(max, method.fixedargs.length + (method.varargs != null ? 2 : 0)); + } + this.methodsByArgCount = new JavaMethod[max + 1][]; + for (int nargs = 0; nargs <= max; nargs++) { + List methodsForCount = new ArrayList<>(); + for (JavaMethod javaMethod : methods) { + int fixed = javaMethod.fixedargs.length; + if (nargs == fixed || nargs > fixed && javaMethod.varargs != null) { + methodsForCount.add(javaMethod); + } + } + methodsByArgCount[nargs] = methodsForCount.isEmpty() ? methods : methodsForCount.toArray(JavaMethod[]::new); + } + } + + public LuaValue call() { + return error("method cannot be called without instance"); + } + + public LuaValue call(LuaValue arg) { + return invokeBestMethod(arg.checkuserdata(), LuaValue.NONE); + } + + public LuaValue call(LuaValue arg1, LuaValue arg2) { + return invokeBestMethod(arg1.checkuserdata(), arg2); + } + + public LuaValue call(LuaValue arg1, LuaValue arg2, LuaValue arg3) { + return invokeBestMethod(arg1.checkuserdata(), LuaValue.varargsOf(arg2, arg3)); + } + + public Varargs invoke(Varargs args) { + return invokeBestMethod(args.checkuserdata(1), args.subargs(2)); + } + + private LuaValue invokeBestMethod(Object instance, Varargs args) { + JavaMethod best = null; + + // first pass: filter possible methods by number of args provided + JavaMethod[] methods = methodsByArgCount[Math.min(methodsByArgCount.length - 1, args.narg())]; + if (methods.length == 1) { + best = methods[0]; + } else if (methods.length != 0) { + int score = Integer.MAX_VALUE; + // if there are multiple, then pick the one with the best score + for (JavaMethod javaMethod : methods) { + int s = javaMethod.score(args); + if (s < score) { + score = s; + best = javaMethod; + if (score == 0) + break; + } + } + } + + // any match? + if (best == null) + LuaValue.error("no coercible public method"); + + // invoke it + return best.invokeMethod(instance, args); + } + } + +} diff --git a/planetiler-experimental/src/main/java/org/luaj/vm2/lib/jse/LuaBindMethods.java b/planetiler-experimental/src/main/java/org/luaj/vm2/lib/jse/LuaBindMethods.java new file mode 100644 index 0000000000..a9dd24ec26 --- /dev/null +++ b/planetiler-experimental/src/main/java/org/luaj/vm2/lib/jse/LuaBindMethods.java @@ -0,0 +1,15 @@ +package org.luaj.vm2.lib.jse; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Annotation that allows methods on a class to be called from lua with instance.method(), or for those methods to be + * detached from the instance and called on their own. + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +public @interface LuaBindMethods { +} diff --git a/planetiler-experimental/src/main/java/org/luaj/vm2/lib/jse/LuaFunctionType.java b/planetiler-experimental/src/main/java/org/luaj/vm2/lib/jse/LuaFunctionType.java new file mode 100644 index 0000000000..27ded01599 --- /dev/null +++ b/planetiler-experimental/src/main/java/org/luaj/vm2/lib/jse/LuaFunctionType.java @@ -0,0 +1,17 @@ +package org.luaj.vm2.lib.jse; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Annotation that that generates the type for a lua value from a method on another class. + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.FIELD) +public @interface LuaFunctionType { + Class target(); + + String method() default ""; +} diff --git a/planetiler-experimental/src/main/java/org/luaj/vm2/lib/jse/LuaGetter.java b/planetiler-experimental/src/main/java/org/luaj/vm2/lib/jse/LuaGetter.java new file mode 100644 index 0000000000..18f637136b --- /dev/null +++ b/planetiler-experimental/src/main/java/org/luaj/vm2/lib/jse/LuaGetter.java @@ -0,0 +1,14 @@ +package org.luaj.vm2.lib.jse; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Annotation that allows a method to intercept calls to get {@code instance.property} from lua. + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.METHOD) +public @interface LuaGetter { +} diff --git a/planetiler-experimental/src/main/java/org/luaj/vm2/lib/jse/LuaSetter.java b/planetiler-experimental/src/main/java/org/luaj/vm2/lib/jse/LuaSetter.java new file mode 100644 index 0000000000..635a12aee5 --- /dev/null +++ b/planetiler-experimental/src/main/java/org/luaj/vm2/lib/jse/LuaSetter.java @@ -0,0 +1,14 @@ +package org.luaj.vm2.lib.jse; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Annotation that allows a method to intercept calls to set {@code instance.property = value} from lua. + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.METHOD) +public @interface LuaSetter { +} diff --git a/planetiler-experimental/src/main/java/org/luaj/vm2/lib/jse/LuaType.java b/planetiler-experimental/src/main/java/org/luaj/vm2/lib/jse/LuaType.java new file mode 100644 index 0000000000..c31d782d30 --- /dev/null +++ b/planetiler-experimental/src/main/java/org/luaj/vm2/lib/jse/LuaType.java @@ -0,0 +1,15 @@ +package org.luaj.vm2.lib.jse; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Annotation that lets you define the type of a lua value. + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.PARAMETER) +public @interface LuaType { + String value(); +} diff --git a/planetiler-experimental/src/test/java/com/onthegomap/planetiler/experimental/lua/GenerateLuaTypesTest.java b/planetiler-experimental/src/test/java/com/onthegomap/planetiler/experimental/lua/GenerateLuaTypesTest.java new file mode 100644 index 0000000000..940244d90e --- /dev/null +++ b/planetiler-experimental/src/test/java/com/onthegomap/planetiler/experimental/lua/GenerateLuaTypesTest.java @@ -0,0 +1,528 @@ +package com.onthegomap.planetiler.experimental.lua; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import com.onthegomap.planetiler.config.Arguments; +import java.nio.file.Path; +import java.util.Date; +import java.util.List; +import java.util.Map; +import java.util.concurrent.Semaphore; +import java.util.function.Consumer; +import org.junit.jupiter.api.Test; +import org.luaj.vm2.LuaDouble; +import org.luaj.vm2.LuaInteger; +import org.luaj.vm2.LuaNumber; +import org.luaj.vm2.LuaString; +import org.luaj.vm2.LuaTable; +import org.luaj.vm2.LuaValue; +import org.luaj.vm2.lib.jse.LuaBindMethods; +import org.luaj.vm2.lib.jse.LuaFunctionType; +import org.luaj.vm2.lib.jse.LuaGetter; +import org.luaj.vm2.lib.jse.LuaSetter; + +@SuppressWarnings("unused") +class GenerateLuaTypesTest { + + @Test + void testSimpleClass() { + interface Test { + + String string(); + + int intMethod(Integer n); + + long longMethod(Long n); + + double doubleMethod(Double n); + + float floatMethod(Float n); + + short shortMethod(Short n); + + byte byteMethod(Byte n); + + boolean booleanMethod(Boolean n); + } + assertGenerated(""" + ---@class (exact) com_onthegomap_planetiler_experimental_lua_GenerateLuaTypesTest_1Test + types.com_onthegomap_planetiler_experimental_lua_GenerateLuaTypesTest_1Test = {} + ---@param n boolean + ---@return boolean + function types.com_onthegomap_planetiler_experimental_lua_GenerateLuaTypesTest_1Test:boolean_method(n) end + ---@param n integer + ---@return integer + function types.com_onthegomap_planetiler_experimental_lua_GenerateLuaTypesTest_1Test:byte_method(n) end + ---@param n number + ---@return number + function types.com_onthegomap_planetiler_experimental_lua_GenerateLuaTypesTest_1Test:double_method(n) end + ---@param n number + ---@return number + function types.com_onthegomap_planetiler_experimental_lua_GenerateLuaTypesTest_1Test:float_method(n) end + ---@param n integer + ---@return integer + function types.com_onthegomap_planetiler_experimental_lua_GenerateLuaTypesTest_1Test:int_method(n) end + ---@param n integer + ---@return integer + function types.com_onthegomap_planetiler_experimental_lua_GenerateLuaTypesTest_1Test:long_method(n) end + ---@param n integer + ---@return integer + function types.com_onthegomap_planetiler_experimental_lua_GenerateLuaTypesTest_1Test:short_method(n) end + ---@return string + function types.com_onthegomap_planetiler_experimental_lua_GenerateLuaTypesTest_1Test:string() end + """, Test.class); + } + + @Test + void testSimpleClassWithLuaBindMethods() { + @LuaBindMethods + interface TestBindMethods { + + String string(); + + int intMethod(Integer n); + + void voidMethod(); + } + assertGenerated(""" + ---@class (exact) com_onthegomap_planetiler_experimental_lua_GenerateLuaTypesTest_1TestBindMethods + ---@field int_method fun(n: integer): integer + ---@field string fun(): string + ---@field void_method fun(): nil + types.com_onthegomap_planetiler_experimental_lua_GenerateLuaTypesTest_1TestBindMethods = {} + """, TestBindMethods.class); + } + + @Test + void testSimpleClassWithField() { + class WithField { + + public Date field; + } + assertGenerated(""" + ---@class (exact) com_onthegomap_planetiler_experimental_lua_GenerateLuaTypesTest_1WithField + ---@field field java_util_Date + types.com_onthegomap_planetiler_experimental_lua_GenerateLuaTypesTest_1WithField = {} + """, WithField.class); + } + + @Test + void testSimpleClassWithGetter() { + class WithGetter { + + @LuaGetter + public Date field() { + return null; + } + + @LuaGetter + public List field2() { + return null; + } + + @LuaSetter + public void field2(List value) {} + } + assertGenerated(""" + ---@class (exact) com_onthegomap_planetiler_experimental_lua_GenerateLuaTypesTest_1WithGetter + ---@field field java_util_Date + ---@field field2 string[] + types.com_onthegomap_planetiler_experimental_lua_GenerateLuaTypesTest_1WithGetter = {} + """, WithGetter.class); + } + + @Test + void testSimpleClassWithSetter() { + class WithSetter { + + @LuaSetter + public void field(Date field) {} + } + assertGenerated(""" + ---@class (exact) com_onthegomap_planetiler_experimental_lua_GenerateLuaTypesTest_1WithSetter + ---@field field java_util_Date + types.com_onthegomap_planetiler_experimental_lua_GenerateLuaTypesTest_1WithSetter = {} + """, WithSetter.class); + } + + @Test + void testArrays() { + class WithArrays { + + public int[] intArray; + public Integer[] intObjectArray; + public Semaphore[] semaphoreArray; + } + assertGenerated(""" + ---@class (exact) com_onthegomap_planetiler_experimental_lua_GenerateLuaTypesTest_1WithArrays + ---@field int_array integer[] + ---@field int_object_array integer[] + ---@field semaphore_array java_util_concurrent_Semaphore[] + types.com_onthegomap_planetiler_experimental_lua_GenerateLuaTypesTest_1WithArrays = {} + """, WithArrays.class); + } + + @Test + void testKeywordCollision() { + class KeywordCollision { + + public int and; + } + assertGenerated( + """ + ---@class (exact) com_onthegomap_planetiler_experimental_lua_GenerateLuaTypesTest_1KeywordCollision + ---@field AND integer + types.com_onthegomap_planetiler_experimental_lua_GenerateLuaTypesTest_1KeywordCollision = {} + """, + KeywordCollision.class); + } + + @Test + void testLists() { + class WithLists { + + public List ints; + public List dates; + public List objs; + public List wildcards; + } + assertGenerated(""" + ---@class (exact) com_onthegomap_planetiler_experimental_lua_GenerateLuaTypesTest_1WithLists + ---@field dates java_util_Date[] + ---@field ints integer[] + ---@field objs any[] + ---@field wildcards any[] + types.com_onthegomap_planetiler_experimental_lua_GenerateLuaTypesTest_1WithLists = {} + """, WithLists.class); + } + + @Test + void testMaps() { + class WithLists { + + public Map stringToInt; + public Map dateToDouble; + public Map objectToWildcard; + public Map>> stringToDoubleStringList; + } + assertGenerated(""" + ---@class (exact) com_onthegomap_planetiler_experimental_lua_GenerateLuaTypesTest_2WithLists + ---@field date_to_double {[java_util_Date]: number} + ---@field object_to_wildcard {[any]: any} + ---@field string_to_double_string_list {[string]: string[][]} + ---@field string_to_int {[string]: integer} + types.com_onthegomap_planetiler_experimental_lua_GenerateLuaTypesTest_2WithLists = {} + """, WithLists.class); + } + + @Test + void testMethodOnGenericType() { + class Generic { + + public T value() { + return null; + } + } + class Concrete extends Generic {} + assertGenerated( + """ + ---@class (exact) com_onthegomap_planetiler_experimental_lua_GenerateLuaTypesTest_1Concrete : com_onthegomap_planetiler_experimental_lua_GenerateLuaTypesTest_1Generic + types.com_onthegomap_planetiler_experimental_lua_GenerateLuaTypesTest_1Concrete = {} + ---@return string + function types.com_onthegomap_planetiler_experimental_lua_GenerateLuaTypesTest_1Concrete:value() end + """, + Concrete.class); + } + + @Test + void testGenericMethod() { + class GenericMethod { + + public T apply(T input) { + return null; + } + + public T applyNumber(T input) { + return null; + } + } + assertGenerated( + """ + ---@class (exact) com_onthegomap_planetiler_experimental_lua_GenerateLuaTypesTest_1GenericMethod + types.com_onthegomap_planetiler_experimental_lua_GenerateLuaTypesTest_1GenericMethod = {} + ---@generic T + ---@param input T + ---@return T + function types.com_onthegomap_planetiler_experimental_lua_GenerateLuaTypesTest_1GenericMethod:apply(input) end + ---@generic T : number + ---@param input T + ---@return T + function types.com_onthegomap_planetiler_experimental_lua_GenerateLuaTypesTest_1GenericMethod:apply_number(input) end + """, + GenericMethod.class); + } + + @Test + void testGenericMethodInterface() { + interface Interface { + + T value(); + } + interface ConcreteInterface extends Interface {} + assertGenerated( + """ + ---@class (exact) com_onthegomap_planetiler_experimental_lua_GenerateLuaTypesTest_1ConcreteInterface : com_onthegomap_planetiler_experimental_lua_GenerateLuaTypesTest_1Interface + types.com_onthegomap_planetiler_experimental_lua_GenerateLuaTypesTest_1ConcreteInterface = {} + ---@return string + function types.com_onthegomap_planetiler_experimental_lua_GenerateLuaTypesTest_1ConcreteInterface:value() end + """, + ConcreteInterface.class); + } + + @Test + void testRecord() { + record Record(int x, int y) {} + assertGenerated( + """ + ---@class (exact) com_onthegomap_planetiler_experimental_lua_GenerateLuaTypesTest_1Record : java_lang_Record + ---@field x integer + ---@field y integer + types.com_onthegomap_planetiler_experimental_lua_GenerateLuaTypesTest_1Record = {} + """, + Record.class); + } + + @Test + void testGenericField() { + class Generic { + + public T field; + } + class ConcreteField extends Generic {} + assertGenerated( + """ + ---@class (exact) com_onthegomap_planetiler_experimental_lua_GenerateLuaTypesTest_1ConcreteField : com_onthegomap_planetiler_experimental_lua_GenerateLuaTypesTest_2Generic + ---@field field integer + types.com_onthegomap_planetiler_experimental_lua_GenerateLuaTypesTest_1ConcreteField = {} + """, + ConcreteField.class); + } + + @Test + void testLuaValueWithAnnotation() { + class Target { + public int method(int arg) { + return 1; + } + } + class LuaValues { + + public LuaString string; + public LuaInteger integer; + public LuaDouble number; + public LuaNumber number2; + public LuaTable table; + @LuaFunctionType( + target = Target.class, + method = "method" + ) + public LuaValue value; + } + assertGenerated( + """ + ---@class (exact) com_onthegomap_planetiler_experimental_lua_GenerateLuaTypesTest_1LuaValues + ---@field integer integer + ---@field number number + ---@field number2 number + ---@field string string + ---@field table table + ---@field value fun(arg: integer): integer + types.com_onthegomap_planetiler_experimental_lua_GenerateLuaTypesTest_1LuaValues = {} + """, + LuaValues.class); + } + + public static class StaticClassSuper { + public static final int SUPER_CONSTANT = 1; + public final int superInstanceField = 1; + + public int superInstance(int arg) { + return 1; + } + + public static String superStaticMethod(String arg) { + return ""; + } + } + + public static class StaticClass extends StaticClassSuper { + public static final int CONSTANT = 1; + public static int staticField; + public int instanceField; + + public StaticClass() {} + + public StaticClass(int i) {} + + public int instance(int arg) { + return 1; + } + + public static int staticMethod(int arg) { + return 1; + } + } + + @Test + void testStaticLuaInstanceWithConsructors() { + assertGeneratedStatic( + """ + ---@class (exact) com_onthegomap_planetiler_experimental_lua_GenerateLuaTypesTest_StaticClass__class + ---@field CONSTANT integer + ---@field SUPER_CONSTANT integer + ---@field static_field integer + types.com_onthegomap_planetiler_experimental_lua_GenerateLuaTypesTest_StaticClass__class = {} + ---@param i integer + ---@return com_onthegomap_planetiler_experimental_lua_GenerateLuaTypesTest_StaticClass + function types.com_onthegomap_planetiler_experimental_lua_GenerateLuaTypesTest_StaticClass__class:new(i) end + ---@return com_onthegomap_planetiler_experimental_lua_GenerateLuaTypesTest_StaticClass + function types.com_onthegomap_planetiler_experimental_lua_GenerateLuaTypesTest_StaticClass__class:new() end + ---@param arg integer + ---@return integer + function types.com_onthegomap_planetiler_experimental_lua_GenerateLuaTypesTest_StaticClass__class:static_method(arg) end + ---@param arg string + ---@return string + function types.com_onthegomap_planetiler_experimental_lua_GenerateLuaTypesTest_StaticClass__class:super_static_method(arg) end + """ + .trim(), + StaticClass.class); + } + + @Test + void testGenericClassMethod() { + assertGenerated( + """ + ---@class (exact) java_util_function_Consumer + types.java_util_function_Consumer = {} + ---@param arg0 any + ---@return nil + function types.java_util_function_Consumer:accept(arg0) end + ---@param arg0 java_util_function_Consumer + ---@return java_util_function_Consumer + function types.java_util_function_Consumer:and_then(arg0) end + """, + Consumer.class); + } + + @Test + void testEnum() { + enum TestEnum { + A, + B, + C + } + assertGenerated( + """ + ---@alias com_onthegomap_planetiler_experimental_lua_GenerateLuaTypesTest_1TestEnum + ---|com_onthegomap_planetiler_experimental_lua_GenerateLuaTypesTest_1TestEnum__enum + ---|integer + ---|"A" + ---|"B" + ---|"C" + ---@class (exact) com_onthegomap_planetiler_experimental_lua_GenerateLuaTypesTest_1TestEnum__enum : java_lang_Enum + types.com_onthegomap_planetiler_experimental_lua_GenerateLuaTypesTest_1TestEnum__enum = {} + ---@param arg0 any + ---@return integer + function types.com_onthegomap_planetiler_experimental_lua_GenerateLuaTypesTest_1TestEnum__enum:compare_to(arg0) end + ---@param arg0 com_onthegomap_planetiler_experimental_lua_GenerateLuaTypesTest_1TestEnum + ---@return integer + function types.com_onthegomap_planetiler_experimental_lua_GenerateLuaTypesTest_1TestEnum__enum:compare_to(arg0) end + ---@return java_util_Optional + function types.com_onthegomap_planetiler_experimental_lua_GenerateLuaTypesTest_1TestEnum__enum:describe_constable() end + ---@return userdata + function types.com_onthegomap_planetiler_experimental_lua_GenerateLuaTypesTest_1TestEnum__enum:get_declaring_class() end + ---@return string + function types.com_onthegomap_planetiler_experimental_lua_GenerateLuaTypesTest_1TestEnum__enum:name() end + ---@return integer + function types.com_onthegomap_planetiler_experimental_lua_GenerateLuaTypesTest_1TestEnum__enum:ordinal() end + """, + TestEnum.class); + class UsesEnum { + public TestEnum field; + + public TestEnum method(TestEnum arg) { + return null; + } + } + assertGenerated( + """ + ---@class (exact) com_onthegomap_planetiler_experimental_lua_GenerateLuaTypesTest_1UsesEnum + ---@field field com_onthegomap_planetiler_experimental_lua_GenerateLuaTypesTest_1TestEnum + types.com_onthegomap_planetiler_experimental_lua_GenerateLuaTypesTest_1UsesEnum = {} + ---@param arg com_onthegomap_planetiler_experimental_lua_GenerateLuaTypesTest_1TestEnum + ---@return com_onthegomap_planetiler_experimental_lua_GenerateLuaTypesTest_1TestEnum + function types.com_onthegomap_planetiler_experimental_lua_GenerateLuaTypesTest_1UsesEnum:method(arg) end + """, + UsesEnum.class); + } + + @Test + void testPath() { + class UsesPath { + public Path field; + + public Path method(Path arg) { + return null; + } + } + assertGenerated( + """ + ---@class (exact) com_onthegomap_planetiler_experimental_lua_GenerateLuaTypesTest_1UsesPath + ---@field field java_nio_file_Path + types.com_onthegomap_planetiler_experimental_lua_GenerateLuaTypesTest_1UsesPath = {} + ---@param arg java_nio_file_Path|string|string[] + ---@return java_nio_file_Path + function types.com_onthegomap_planetiler_experimental_lua_GenerateLuaTypesTest_1UsesPath:method(arg) end + """, + UsesPath.class); + } + + @Test + void testAbbreviationInName() { + interface Test { + + int setSRID(Integer n); + } + assertGenerated(""" + ---@class (exact) com_onthegomap_planetiler_experimental_lua_GenerateLuaTypesTest_2Test + types.com_onthegomap_planetiler_experimental_lua_GenerateLuaTypesTest_2Test = {} + ---@param n integer + ---@return integer + function types.com_onthegomap_planetiler_experimental_lua_GenerateLuaTypesTest_2Test:set_srid(n) end + """, Test.class); + } + + @Test + void testGeneratedMetaFileCompiles() { + String types = new GenerateLuaTypes().generatePlanetiler().toString(); + LuaEnvironment.loadScript(Arguments.of("luajc", "false"), types, "types.lua"); + } + + private static void assertGenerated(String expected, Class clazz) { + assertGenerated(expected, clazz, false); + } + + private static void assertGeneratedStatic(String expected, Class clazz) { + assertGenerated(expected, clazz, true); + } + + private static void assertGenerated(String expected, Class clazz, boolean staticType) { + var g = new GenerateLuaTypes(); + var actual = (staticType ? g.getStaticTypeDefinition(clazz) : g.getTypeDefinition(clazz)).trim(); + assertEquals(fixNewlines(expected.trim()), fixNewlines(actual), "got:%n%n%s%n%n".formatted(actual)); + } + + private static String fixNewlines(String input) { + return input.replaceAll("[\n\r]+", System.lineSeparator()); + } +} diff --git a/planetiler-experimental/src/test/java/com/onthegomap/planetiler/experimental/lua/JavaToLuaCaseTest.java b/planetiler-experimental/src/test/java/com/onthegomap/planetiler/experimental/lua/JavaToLuaCaseTest.java new file mode 100644 index 0000000000..92c53a186f --- /dev/null +++ b/planetiler-experimental/src/test/java/com/onthegomap/planetiler/experimental/lua/JavaToLuaCaseTest.java @@ -0,0 +1,36 @@ +package com.onthegomap.planetiler.experimental.lua; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; + +class JavaToLuaCaseTest { + @ParameterizedTest + @CsvSource({ + "foo, foo", + "Foo, Foo", + "fooBar, foo_bar", + "FooBar, FooBar", + "foo_bar, foo_bar", + "foo_BAR, foo_BAR", + "FOO_BAR, FOO_BAR", + "fooBAR, foo_bar", + "getISO3Code, get_iso3_code", + "utf8string, utf8_string", + "arg0, arg0", + "arg10, arg10", + "utf8String, utf8_string", + "getUTF8String, get_utf8_string", + "getUTF8string, get_utf8_string", + "getUTF8ASCIIString, get_utf8ascii_string", + "iso31661Alpha2, iso31661_alpha2", + "getUTF8At0, get_utf8_at0", + "distance3D, distance3d", + "toASCIIString, to_ascii_string", + "and, AND", + }) + void testConversions(String input, String expectedOutput) { + assertEquals(expectedOutput, JavaToLuaCase.transformMemberName(input)); + } +} diff --git a/planetiler-experimental/src/test/java/com/onthegomap/planetiler/experimental/lua/LuaEnvironmentTests.java b/planetiler-experimental/src/test/java/com/onthegomap/planetiler/experimental/lua/LuaEnvironmentTests.java new file mode 100644 index 0000000000..71d978391a --- /dev/null +++ b/planetiler-experimental/src/test/java/com/onthegomap/planetiler/experimental/lua/LuaEnvironmentTests.java @@ -0,0 +1,852 @@ +package com.onthegomap.planetiler.experimental.lua; + +import static com.onthegomap.planetiler.experimental.lua.LuaConversions.toJava; +import static com.onthegomap.planetiler.experimental.lua.LuaConversions.toLua; +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import com.onthegomap.planetiler.config.Arguments; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.IntStream; +import java.util.stream.Stream; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import org.luaj.vm2.LuaValue; +import org.luaj.vm2.lib.jse.LuaGetter; +import org.luaj.vm2.lib.jse.LuaSetter; + +@SuppressWarnings("unused") +class LuaEnvironmentTests { + @Test + void testCallMethod() { + var env = load(""" + function main() + return 1 + end + """); + assertConvertsTo(1, env.main.call()); + assertConvertsTo("1", env.main.call()); + assertConvertsTo(1L, env.main.call()); + } + + @Test + void testBindProfile() { + var env = load(""" + planetiler.output.name = "name" + planetiler.output.attribution = "attribution" + planetiler.output.description = "description" + planetiler.output.version = "version" + + function planetiler.process_feature() + return 1 + end + function planetiler.finish() + return 1 + end + """); + assertConvertsTo(1, env.planetiler.process_feature.call()); + assertConvertsTo(1, env.planetiler.finish.call()); + assertEquals("name", env.planetiler.output.name); + assertEquals("attribution", env.planetiler.output.attribution); + assertEquals("description", env.planetiler.output.description); + assertEquals("version", env.planetiler.output.version); + } + + @Test + void testOutputPath() { + var env = load(""" + """); + assertEquals(Path.of("data", "output.mbtiles"), env.planetiler.output.path); + var env2 = load(""" + planetiler.output.path = "output.pmtiles" + """); + assertEquals(Path.of("output.pmtiles"), env2.planetiler.output.path); + } + + @Test + void testCallExposedClassMethod() { + var env = load(""" + function main() + return GeoUtils:meters_to_pixel_at_equator(14, 150) + end + """); + assertEquals(15.699197, env.main.call().todouble(), 1e-5); + } + + @Test + void testCallJavaMethod() { + var env = load(""" + function main() + return obj:call(1) + 1 + end + """, Map.of("obj", new Object() { + public int call(int arg) { + return arg + 1; + } + })); + assertConvertsTo(3, env.main.call()); + } + + @Test + void testCallJavaMethodUsingLowerSnakeCase() { + var env = load(""" + function main() + return obj:call_method(1) + 1 + end + """, Map.of("obj", new Object() { + public int callMethod(int arg) { + return arg + 1; + } + })); + assertConvertsTo(3, env.main.call()); + } + + @Test + void testCallJavaMethodWith4Args() { + var env = load(""" + function main() + return obj:call(1, 2, 3, 4) + 1 + end + """, Map.of("obj", new Object() { + public int call(int a, int b, int c, int d) { + return a + b + c + d; + } + })); + assertConvertsTo(11, env.main.call()); + } + + @Test + void testPassArrayToJava() { + var env = load(""" + function main() + return obj:call({1, 2, 3}) + 1 + end + """, Map.of("obj", new Object() { + public int call(int[] args) { + return IntStream.of(args).sum(); + } + })); + assertConvertsTo(7, env.main.call()); + } + + @Test + void testPassBoxedArrayToJava() { + var env = load(""" + function main() + return obj:call({1, 2, 3}) + 1 + end + """, Map.of("obj", new Object() { + public int call(Integer[] args) { + return Stream.of(args).mapToInt(i -> i).sum(); + } + })); + assertConvertsTo(7, env.main.call()); + } + + @Test + void testPassArrayToLua() { + var env = load(""" + function main() + return obj:call({1, 2, 3}) + end + """, Map.of("obj", new Object() { + public int[] call(int[] args) { + return args; + } + })); + assertArrayEquals(new int[]{1, 2, 3}, toJava(env.main.call(), int[].class)); + } + + @Test + void passListToLua() { + var env = load(""" + function main() + return obj:call({1, 2, 3}) + 1 + end + """, Map.of("obj", new Object() { + public int call(List args) { + return args.stream().mapToInt(i -> i).sum(); + } + })); + assertConvertsTo(7, env.main.call()); + } + + @Test + void passJavaListToLua() { + var env = load(""" + function main(arg) + return obj:call(arg) + 1 + end + """, Map.of("obj", new Object() { + public int call(List args) { + return args.stream().mapToInt(i -> i).sum(); + } + })); + assertConvertsTo(7, env.main.call(toLua(List.of(1, 2, 3)))); + } + + @Test + void passListToJava() { + var env = load(""" + function main() + return obj:call({1, 2, 3}) + end + """, Map.of("obj", new Object() { + public List call(int[] args) { + return IntStream.of(args).boxed().toList(); + } + })); + assertConvertsTo(List.of(1, 2, 3), env.main.call()); + } + + @Test + void passCollectionToJava() { + var env = load(""" + function main() + return obj:call({1, 2, 3}) + end + """, Map.of("obj", new Object() { + public int call(Collection args) { + return args.stream().mapToInt(i -> i).sum(); + } + })); + assertConvertsTo(6, env.main.call()); + } + + @Test + void passSetToJava() { + var env = load(""" + function main() + return obj:call({1, 2, 3, 3}) + end + """, Map.of("obj", new Object() { + public int call(Set args) { + return args.stream().mapToInt(i -> i).sum(); + } + })); + assertConvertsTo(6, env.main.call()); + } + + @Test + void passSetFromTableToJava() { + var env = load(""" + function main() + return obj:call({[1] = true, [2] = true, [3] = true}) + end + """, Map.of("obj", new Object() { + public int call(Set args) { + return args.stream().mapToInt(i -> i).sum(); + } + })); + assertConvertsTo(6, env.main.call()); + } + + @Test + void passMapToJava() { + var env = load(""" + function main() + return obj:call({[1] = "one", [2] = "two", [3] = "three"}) + end + """, Map.of("obj", new Object() { + public String call(Map args) { + return args.get(1) + " " + args.get(3); + } + })); + assertConvertsTo("one three", env.main.call()); + } + + @Test + void testCallPrimitiveJavaVarArgsMethod() { + var env = load(""" + function main() + return obj:call(1, 2, 3) + 1 + end + """, Map.of("obj", new Object() { + public int call(int... args) { + return IntStream.of(args).sum(); + } + })); + assertConvertsTo(7, env.main.call()); + } + + @Test + void testCallBoxedJavaVarArgsMethod() { + var env = load(""" + function main() + return obj:call(1, 2, 3) + 1 + end + """, Map.of("obj", new Object() { + public int call(Integer... args) { + return Stream.of(args).mapToInt(i -> i).sum(); + } + })); + assertConvertsTo(7, env.main.call()); + } + + @Test + void chooseVarArgMethodOverOthers() { + var env = load(""" + function main() + return { + obj:call(), + obj:call(1), + obj:call(1, 2), + obj:call(1, 2, 3), + obj:call(1, 2, 3, 4), + obj:call(1, 2, 3, 4, 5), + obj:call(1, 2, 3, 4, 5, 6), + obj:call(1, 2, 3, 4, 5, 6, 7), + obj:call(1, 2, 3, 4, 5, 6, 7, 8), + obj:call(1, 2, 3, 4, 5, 6, 7, 8, 9), + obj:call(1, 2, 3, 4, 5, 6, 7, 8, 9, 10), + } + end + """, Map.of("obj", new Object() { + public int call() { + return 0; + } + + public int call(int a) { + return a; + } + + public int call(int a, int b) { + return a + b; + } + + public int call(int a, int b, int c) { + return a + b + c; + } + + public int call(int a, int b, int c, int d) { + return a + b + c + d; + } + + public int call(int a, int b, int c, int d, int... rest) { + return a + b + c + d + IntStream.of(rest).sum(); + } + })); + assertConvertsTo(List.class, List.of( + 0, 1, 3, 6, 10, 15, 21, 28, 36, 45, 55 + ), env.main.call()); + } + + @Test + void chooseVarArgMethodOverOthersBoxed() { + var env = load(""" + function main() + return { + obj:call(), + obj:call(1), + obj:call(1, 2), + obj:call(1, 2, 3), + obj:call(1, 2, 3, 4), + obj:call(1, 2, 3, 4, 5), + obj:call(1, 2, 3, 4, 5, 6), + obj:call(1, 2, 3, 4, 5, 6, 7), + obj:call(1, 2, 3, 4, 5, 6, 7, 8), + obj:call(1, 2, 3, 4, 5, 6, 7, 8, 9), + obj:call(1, 2, 3, 4, 5, 6, 7, 8, 9, 10), + } + end + """, Map.of("obj", new Object() { + public Integer call() { + return 0; + } + + public Integer call(Integer a) { + return a; + } + + public Integer call(Integer a, Integer b) { + return a + b; + } + + public Integer call(Integer a, Integer b, Integer c) { + return a + b + c; + } + + public Integer call(Integer a, Integer b, Integer c, Integer d) { + return a + b + c + d; + } + + public Integer call(Integer a, Integer b, Integer c, Integer d, Integer... rest) { + return a + b + c + d + Stream.of(rest).mapToInt(i -> i).sum(); + } + })); + assertConvertsTo(List.class, List.of( + 0, 1, 3, 6, 10, 15, 21, 28, 36, 45, 55 + ), env.main.call()); + } + + @Test + void testCoercesPathFromString() { + var env = load(""" + function main() + return obj:call("test.java") + end + """, Map.of("obj", new Object() { + public String call(Path path) { + return path.toString(); + } + })); + assertConvertsTo("test.java", env.main.call()); + } + + @Test + void testCoercesPathFromList() { + var env = load(""" + function main() + return obj:call({"a", "b", "c.java"}) + end + """, Map.of("obj", new Object() { + public Path call(Path path) { + return path; + } + })); + assertConvertsTo(Path.of("a", "b", "c.java"), env.main.call()); + } + + @Test + void testGettersAndSetters() { + var obj = new Object() { + public int value = 0; + private int value2 = 0; + + @LuaSetter + public void value2(int v) { + this.value2 = v; + } + + @LuaGetter + public int value2() { + return this.value2 + 1; + } + }; + var env = load(""" + function main() + obj.value = 1 + obj.value2 = 2; + return obj.value2; + end + """, Map.of("obj", obj)); + assertConvertsTo(3, env.main.call()); + assertEquals(1, obj.value); + assertEquals(2, obj.value2); + } + + @Test + void testRecord() { + record Record(int a, String b) { + public int c() { + return a + 1; + } + } + var env = load(""" + function main() + return {obj.a + 1, obj.b .. 1, obj:c()}; + end + """, Map.of("obj", new Record(1, "2"))); + assertConvertsTo(List.class, List.of(2, "21", 2), env.main.call()); + } + + @Test + void testSetLanguages() { + var env = load(""" + planetiler.languages = {"en", "es"} + """); + assertEquals(List.of("en", "es"), env.runner.getDefaultLanguages()); + } + + @Test + void testFetchWikidataTranslations() { + var env = load(""" + planetiler.fetch_wikidata_translations() + planetiler.fetch_wikidata_translations("data/sources/translations.json") + """); + } + + @Test + void testAddSource() { + var env = load(""" + planetiler.add_source('osm', { + type = 'osm', + path = 'file.osm.pbf' + }) + """); + } + + @Test + void testReadArg() { + var env = LuaEnvironment.loadScript(Arguments.of( + "key", "value" + ), """ + function main() + return planetiler.args:get_string("key") + end + """, "script.lua", Map.of()); + assertConvertsTo("value", env.main.call()); + } + + @Test + void testReadConfigRecord() { + var env = LuaEnvironment.loadScript(Arguments.of( + "threads", "99" + ), """ + function main() + return planetiler.config.threads + end + """, "script.lua", Map.of()); + assertConvertsTo(99, env.main.call()); + } + + @Test + void testTranslations() { + var env = load(""" + planetiler.languages = {"en", "es"} + function main() + return planetiler.translations:get_translations({ + ['name:en'] = "english name", + ['name:es'] = "spanish name", + ['name:de'] = "german name", + }) + end + """); + assertConvertsTo(Map.class, Map.of( + "name:en", "english name", + "name:es", "spanish name" + ), env.main.call()); + } + + @Test + void transliterate() { + var env = load(""" + function main() + return planetiler.translations:transliterate("日本") + end + """); + assertConvertsTo("rì běn", env.main.call()); + } + + @Test + void testStats() { + var env = load(""" + planetiler.stats:data_error('lua_error') + """); + assertEquals(Map.of( + "lua_error", 1L + ), env.planetiler.stats.dataErrors()); + } + + @Test + void testIterateOverList() { + var env = load(""" + function main() + local result = 0 + for i, match in ipairs(obj:call()) do + result = result + match + end + return result + end + """, Map.of("obj", new Object() { + public List call() { + return List.of(1, 2, 3); + } + })); + assertConvertsTo(6, env.main.call()); + } + + @Test + void testIterateOverMap() { + var env = load(""" + function main() + local result = 0 + for k, v in pairs(obj:call()) do + result = result + k + v + end + return result + end + """, Map.of("obj", new Object() { + public Map call() { + return Map.of(1, 2, 3, 4); + } + })); + assertConvertsTo(10, env.main.call()); + } + + @Test + void testInvokeReservedKeyword() { + var env = load(""" + function main() + return obj:AND() + end + """, Map.of("obj", new Object() { + public int and() { + return 1; + } + })); + assertConvertsTo(1, env.main.call()); + } + + @Test + void testPickVarArgOverList() { + var env = load(""" + function main() + return {obj:call(), obj:call('a'), obj:call({'a'})} + end + """, Map.of("obj", new Object() { + public int call(String... args) { + return 1; + } + + public int call(List args) { + return 2; + } + })); + assertConvertsTo(List.class, List.of(1, 1, 2), env.main.call()); + } + + @Test + void testPickVarArgOverListAfterFirstArg() { + var env = load(""" + function main() + return {obj:call('a'), obj:call('a', 'a'), obj:call('a', {'a'})} + end + """, Map.of("obj", new Object() { + public int call(String arg, String... args) { + return 1; + } + + public int call(String arg, List args) { + return 2; + } + })); + assertConvertsTo(List.class, List.of(1, 1, 2), env.main.call()); + } + + @ParameterizedTest + @ValueSource(ints = {0, 1, 2}) + void testGetWithIndexFromList(int idx) { + var env = load(""" + function main() + return obj:call()[%d] + end + """.formatted(idx), Map.of("obj", new Object() { + public List call() { + return List.of(1); + } + })); + assertConvertsTo(Integer.class, idx == 1 ? 1 : 0, env.main.call()); + } + + @Test + void testSetWithIndexFromList() { + var env = load(""" + function main() + local list = obj:call() + list[1] = 2 + return list[1] + end + """, Map.of("obj", new Object() { + public List call() { + return new ArrayList<>(List.of(1)); + } + })); + assertConvertsTo(2, env.main.call()); + } + + @ParameterizedTest + @ValueSource(strings = {"a", "b", "c"}) + void testGetFromMap(String value) { + var env = load(""" + function main() + return obj:call()["%s"] + end + """.formatted(value), Map.of("obj", new Object() { + public Map call() { + return Map.of("b", 1); + } + })); + assertConvertsTo(Integer.class, value.equals("b") ? 1 : 0, env.main.call()); + } + + @Test + void testSetMap() { + var env = load(""" + function main() + local list = obj:call() + list["a"] = "c" + return list["a"] + end + """, Map.of("obj", new Object() { + public Map call() { + return new HashMap<>(Map.of("a", "a")); + } + })); + assertConvertsTo("c", env.main.call()); + } + + @ParameterizedTest + @ValueSource(ints = {0, 1, 2}) + void testGetFromArray(int idx) { + var env = load(""" + function main() + return obj:call()[%d] + end + """.formatted(idx), Map.of("obj", new Object() { + public int[] call() { + return new int[]{1}; + } + })); + assertConvertsTo(Integer.class, idx == 1 ? 1 : 0, env.main.call()); + } + + @Test + void testSetArray() { + var env = load(""" + function main() + local list = obj:call() + list[1] = 2 + return list[1] + end + """, Map.of("obj", new Object() { + public int[] call() { + return new int[]{1}; + } + })); + assertConvertsTo(2, env.main.call()); + } + + @Test + void testEmoji() { + var env = load(""" + function main() + return '👍' + end + """, Map.of("obj", new Object() { + public int[] call() { + return new int[]{1}; + } + })); + assertConvertsTo("👍", env.main.call()); + } + + @Test + void testArrayLength() { + var env = load(""" + function main() + return #{1} + end + """, Map.of("obj", new Object() { + public int[] call() { + return new int[]{1}; + } + })); + assertConvertsTo(1, env.main.call()); + } + + @Test + void testJavaArrayLength() { + var env = load(""" + function main() + return #obj:call() + end + """, Map.of("obj", new Object() { + public int[] call() { + return new int[]{1}; + } + })); + assertConvertsTo(1, env.main.call()); + } + + @Test + void testJavaListLength() { + var env = load(""" + function main() + return #obj:call() + end + """, Map.of("obj", new Object() { + public List call() { + return List.of(1); + } + })); + assertConvertsTo(1, env.main.call()); + } + + @Test + void testAbbreviation() { + var env = load(""" + function main() + return obj:set_srid(1) + end + """, Map.of("obj", new Object() { + public int setSRID(int a) { + return a + 1; + } + })); + assertConvertsTo(2, env.main.call()); + } + + @Test + void testEnum() { + enum MyEnum { + A, + B, + C + }; + var env = load(""" + function main() + return { + obj:call(0), + obj:call('B'), + obj:call(enum.C), + obj:call2(0) + } + end + """, Map.of("enum", MyEnum.class, "obj", new Object() { + + public int call(MyEnum e) { + return e.ordinal(); + } + + public MyEnum call2(int ordinal) { + return MyEnum.values()[ordinal]; + } + })); + assertConvertsTo(List.class, List.of( + 0, 1, 2, MyEnum.A + ), env.main.call()); + } + + public static class MyObj { + public int value = 1; + public static int staticValue = 1; + + public static int get() { + return 1; + } + } + + private static void assertConvertsTo(T java, LuaValue lua) { + assertConvertsTo(java.getClass(), java, lua); + } + + private static void assertConvertsTo(Class clazz, T java, LuaValue lua) { + assertEquals(java, toJava(lua, clazz)); + } + + private static LuaEnvironment load(String script) { + return load(script, Map.of()); + } + + private static LuaEnvironment load(String script, Map extras) { + return LuaEnvironment.loadScript(Arguments.of(), script, "script.lua", extras); + } +} diff --git a/planetiler-experimental/src/test/java/com/onthegomap/planetiler/experimental/lua/LuaProfilesTest.java b/planetiler-experimental/src/test/java/com/onthegomap/planetiler/experimental/lua/LuaProfilesTest.java new file mode 100644 index 0000000000..e51dd0db5e --- /dev/null +++ b/planetiler-experimental/src/test/java/com/onthegomap/planetiler/experimental/lua/LuaProfilesTest.java @@ -0,0 +1,51 @@ +package com.onthegomap.planetiler.experimental.lua; + +import com.onthegomap.planetiler.TestUtils; +import com.onthegomap.planetiler.config.Arguments; +import com.onthegomap.planetiler.validator.SchemaSpecification; +import java.io.IOException; +import java.util.Objects; +import java.util.stream.Stream; +import org.junit.jupiter.api.DynamicNode; +import org.junit.jupiter.api.TestFactory; + +class LuaProfilesTest { + + @TestFactory + Stream testPower() throws IOException { + return validate("power.lua"); + } + + @TestFactory + Stream testRoadsMain() throws IOException { + return validate("roads_main.lua"); + } + + @TestFactory + Stream testRoads() throws IOException { + return validate("roads.lua"); + } + + @TestFactory + Stream testMultifile() throws IOException { + return validate("multifile.lua"); + } + + private static String readResource(String resource) throws IOException { + try (var is = LuaProfilesTest.class.getResourceAsStream(resource)) { + return new String(Objects.requireNonNull(is).readAllBytes()); + } + } + + private static Stream validate(String name) throws IOException { + return validate(name, null); + } + + private static Stream validate(String name, String spec) throws IOException { + LuaEnvironment env = LuaEnvironment.loadScript(Arguments.of(), readResource("/" + name), name); + return TestUtils.validateProfile( + env.profile, + SchemaSpecification.load(readResource("/" + (spec != null ? spec : env.planetiler.examples))) + ); + } +} diff --git a/planetiler-experimental/src/test/java/com/onthegomap/planetiler/experimental/lua/LuaValidatorTest.java b/planetiler-experimental/src/test/java/com/onthegomap/planetiler/experimental/lua/LuaValidatorTest.java new file mode 100644 index 0000000000..11412f4de2 --- /dev/null +++ b/planetiler-experimental/src/test/java/com/onthegomap/planetiler/experimental/lua/LuaValidatorTest.java @@ -0,0 +1,123 @@ +package com.onthegomap.planetiler.experimental.lua; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import com.onthegomap.planetiler.config.Arguments; +import com.onthegomap.planetiler.config.PlanetilerConfig; +import com.onthegomap.planetiler.validator.BaseSchemaValidator; +import com.onthegomap.planetiler.validator.SchemaSpecification; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.PrintStream; +import java.io.UncheckedIOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import org.junit.jupiter.api.io.TempDir; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; + +class LuaValidatorTest { + private final Arguments args = Arguments.of(); + @TempDir + Path tmpDir; + + record Result(BaseSchemaValidator.Result output, String cliOutput) {} + + private Result validate(String schema, String spec) throws IOException { + var result = LuaValidator.validate( + LuaEnvironment.loadScript(args, schema, "schema.lua").profile, + SchemaSpecification.load(spec), + PlanetilerConfig.defaults() + ); + for (var example : result.results()) { + if (example.issues().isFailure()) { + assertNotNull(example.issues().get()); + } + } + Path schemaPath = tmpDir.resolve("schema.lua"); + Path specPath = tmpDir.resolve("spec.yml"); + // also exercise the cli writer and return what it would have printed to stdout + String script = schema + "\nplanetiler.examples= '" + + Files.writeString(specPath, spec).toString().replace("\\", "\\\\").replace("'", "\\'") + "'"; + var cliOutput = validateCli(Files.writeString(schemaPath, script)); + + return new Result(result, cliOutput); + } + + private String validateCli(Path path) { + try ( + var baos = new ByteArrayOutputStream(); + var printStream = new PrintStream(baos, true, StandardCharsets.UTF_8) + ) { + new LuaValidator(args, path.toString(), printStream).validateFromCli(); + return baos.toString(StandardCharsets.UTF_8); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + + @ParameterizedTest + @CsvSource(value = { + "true,water,polygon,natural: water,", + "true,water,polygon,,", + "true,water,polygon,'natural: water\nother: null',", + "false,water,polygon,natural: null,", + "false,water2,polygon,natural: water,", + "false,water,line,natural: water,", + "false,water,line,natural: water,", + "false,water,polygon,natural: water2,", + "false,water,polygon,'natural: water\nother: value',", + + "true,water,polygon,natural: water,allow_extra_tags: true", + "true,water,polygon,natural: water,allow_extra_tags: false", + "true,water,polygon,,allow_extra_tags: true", + "false,water,polygon,,allow_extra_tags: false", + + "true,water,polygon,,min_size: 10", + "false,water,polygon,,min_size: 9", + }) + void testValidateWaterPolygon(boolean shouldBeOk, String layer, String geometry, String tags, String allowExtraTags) + throws IOException { + var results = validate( + """ + function planetiler.process_feature(source, features) + if source:can_be_polygon() and source:has_tag("natural", "water") then + features:polygon("water") + :inherit_attr_from_source("natural") + :set_min_pixel_size(10) + end + end + function main() end + """, + """ + examples: + - name: test output + input: + source: osm + geometry: polygon + tags: + natural: water + output: + layer: %s + geometry: %s + %s + tags: + %s + """.formatted(layer, geometry, allowExtraTags == null ? "" : allowExtraTags, + tags == null ? "" : tags.indent(6).strip()) + ); + assertEquals(1, results.output.results().size()); + assertEquals("test output", results.output.results().get(0).example().name()); + if (shouldBeOk) { + assertTrue(results.output.ok(), results.toString()); + assertFalse(results.cliOutput.contains("FAIL"), "contained FAIL but should not have: " + results.cliOutput); + } else { + assertFalse(results.output.ok(), "Expected an issue, but there were none"); + assertTrue(results.cliOutput.contains("FAIL"), "did not contain FAIL but should have: " + results.cliOutput); + } + } +} diff --git a/planetiler-experimental/src/test/java/com/onthegomap/planetiler/experimental/lua/PlanetilerLuaTest.java b/planetiler-experimental/src/test/java/com/onthegomap/planetiler/experimental/lua/PlanetilerLuaTest.java new file mode 100644 index 0000000000..883c53b2af --- /dev/null +++ b/planetiler-experimental/src/test/java/com/onthegomap/planetiler/experimental/lua/PlanetilerLuaTest.java @@ -0,0 +1,119 @@ +package com.onthegomap.planetiler.experimental.lua; + +import static com.onthegomap.planetiler.TestUtils.assertContains; +import static com.onthegomap.planetiler.util.Gzip.gunzip; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import com.onthegomap.planetiler.TestUtils; +import com.onthegomap.planetiler.VectorTile; +import com.onthegomap.planetiler.mbtiles.Mbtiles; +import java.io.IOException; +import java.nio.file.Path; +import java.util.Map; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; +import org.junit.jupiter.api.io.TempDir; +import org.locationtech.jts.geom.Envelope; +import org.locationtech.jts.geom.Geometry; +import org.locationtech.jts.geom.LineString; + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +abstract class PlanetilerLuaTest { + + private final String script; + + PlanetilerLuaTest(String script) { + this.script = script; + } + + static class WithMainTest extends PlanetilerLuaTest { + WithMainTest() { + super("roads_main.lua"); + } + } + static class WithoutMainTest extends PlanetilerLuaTest { + WithoutMainTest() { + super("roads.lua"); + } + } + + public static final Envelope MONACO_BOUNDS = new Envelope(7.40921, 7.44864, 43.72335, 43.75169); + + @TempDir + static Path tmpDir; + private Mbtiles mbtiles; + + @BeforeAll + void runPlanetiler() throws Exception { + Path dbPath = tmpDir.resolve("output.mbtiles"); + LuaMain.main( + // Use local data extracts instead of downloading + "--script=" + pathToResource(script), + "--osm_path=" + TestUtils.pathToResource("monaco-latest.osm.pbf"), + + // Override temp dir location + "--tmp=" + tmpDir, + + // Override output location + "--output=" + dbPath + ); + mbtiles = Mbtiles.newReadOnlyDatabase(dbPath); + } + + @AfterAll + public void close() throws IOException { + mbtiles.close(); + } + + @Test + void testMetadata() { + Map metadata = mbtiles.metadataTable().getAll(); + assertEquals("Road Schema", metadata.get("name")); + assertEquals("0", metadata.get("minzoom")); + assertEquals("14", metadata.get("maxzoom")); + assertEquals("baselayer", metadata.get("type")); + assertEquals("pbf", metadata.get("format")); + assertEquals("7.40921,43.72335,7.44864,43.75169", metadata.get("bounds")); + assertEquals("7.42892,43.73752,14", metadata.get("center")); + assertContains("Simple", metadata.get("description")); + assertContains("www.openstreetmap.org/copyright", metadata.get("attribution")); + } + + @Test + void ensureValidGeometries() throws Exception { + var parsedTiles = TestUtils.getTiles(mbtiles); + for (var tileEntry : parsedTiles) { + var decoded = VectorTile.decode(gunzip(tileEntry.bytes())); + for (VectorTile.Feature feature : decoded) { + TestUtils.validateGeometry(feature.geometry().decode()); + } + } + } + + @Test + void testRoads() { + assertMinFeatures("road", Map.of( + "highway", "primary" + ), 14, 317, LineString.class); + assertMinFeatures("road", Map.of( + "highway", "service" + ), 14, 310, LineString.class); + } + + private void assertMinFeatures(String layer, Map attrs, int zoom, + int expected, Class clazz) { + TestUtils.assertMinFeatureCount(mbtiles, layer, zoom, attrs, MONACO_BOUNDS, expected, clazz); + } + + public static Path pathToResource(String resource) { + return resolve(Path.of("planetiler-experimental", "src", "test", "resources", resource)); + } + + + private static Path resolve(Path pathFromRoot) { + Path cwd = Path.of("").toAbsolutePath(); + return cwd.resolveSibling(pathFromRoot); + } +} diff --git a/planetiler-experimental/src/test/resources/multifile.lua b/planetiler-experimental/src/test/resources/multifile.lua new file mode 100644 index 0000000000..863a203b80 --- /dev/null +++ b/planetiler-experimental/src/test/resources/multifile.lua @@ -0,0 +1,27 @@ +-- example profile that delegates handling for individual layers to separate files +planetiler.examples = "multifile.spec.yaml" + +planetiler.add_source('osm', { + type = 'osm', + url = 'geofabrik:monaco', +}) + +local layers = { + require("multifile_building"), + require("multifile_housenumber"), +} + +-- TODO make a java utility that does this in a more complete, less verbose way +-- (handle other profile methods, separate handler methods per source, expose layer name, etc.) +local processors = {} +for i, layer in ipairs(layers) do + -- classes defined in LuaEnvironment.CLASSES_TO_EXPOSE are exposed as global variables to the profile + table.insert(processors, MultiExpression:entry(layer.process_feature, layer.filter)) +end +local feature_processors = MultiExpression:of(processors):index() + +function planetiler.process_feature(source, features) + for i, match in ipairs(feature_processors:get_matches_with_triggers(source)) do + match.match(source, features, match.keys) + end +end diff --git a/planetiler-experimental/src/test/resources/multifile.spec.yaml b/planetiler-experimental/src/test/resources/multifile.spec.yaml new file mode 100644 index 0000000000..4a53ddbcf7 --- /dev/null +++ b/planetiler-experimental/src/test/resources/multifile.spec.yaml @@ -0,0 +1,43 @@ +# test cases for multifile.lua +examples: +- name: building + input: + geometry: polygon + source: osm + tags: + building: yes + output: + min_zoom: 14 + layer: building + tags: + class: building +- name: not building + input: + geometry: polygon + source: osm + tags: + building: no + aeroway: hangar + output: [ ] +- name: airport hangar + input: + geometry: polygon + source: osm + tags: + aeroway: hangar + output: + min_zoom: 14 + layer: building + tags: + class: aeroway +- name: housenumber + input: + geometry: point + source: osm + tags: + addr:housenumber: 123 + output: + min_zoom: 14 + layer: housenumber + tags: + housenumber: 123 diff --git a/planetiler-experimental/src/test/resources/multifile_building.lua b/planetiler-experimental/src/test/resources/multifile_building.lua new file mode 100644 index 0000000000..70babd2a6a --- /dev/null +++ b/planetiler-experimental/src/test/resources/multifile_building.lua @@ -0,0 +1,22 @@ +-- handles building features from the multifile.lua example +local mod = {} + +-- multifile.lua builds an optimized MultiExpression matcher from each layer's filter +-- TODO nicer way to build these? +mod.filter = Expression:AND( + Expression:OR( + Expression:match_field('building'), + Expression:match_any('aeroway', 'building', 'hangar') + ), + Expression:NOT(Expression:OR( + Expression:match_any('building', 'no', 'none') + )) +) +-- when filter matches, this function gets run +function mod.process_feature(source, features, keys) + features:polygon("building") + :set_attr('class', keys[1]) + :set_min_zoom(14) +end + +return mod diff --git a/planetiler-experimental/src/test/resources/multifile_housenumber.lua b/planetiler-experimental/src/test/resources/multifile_housenumber.lua new file mode 100644 index 0000000000..e71a92dff1 --- /dev/null +++ b/planetiler-experimental/src/test/resources/multifile_housenumber.lua @@ -0,0 +1,12 @@ +-- handles addr:housenumber features from the multifile.lua example +local mod = {} + +mod.filter = Expression:match_field('addr:housenumber') + +function mod.process_feature(source, features) + features:point("housenumber") + :set_attr("housenumber", source:get_tag("addr:housenumber")) + :set_min_zoom(14) +end + +return mod diff --git a/planetiler-experimental/src/test/resources/power.lua b/planetiler-experimental/src/test/resources/power.lua new file mode 100644 index 0000000000..bd2f6a6649 --- /dev/null +++ b/planetiler-experimental/src/test/resources/power.lua @@ -0,0 +1,40 @@ +-- Example lua profile that emits power lines and poles from an openstreetmap source +-- useful for hot air ballooning + +-- The planetiler object defined in LuaEnvironment.PlanetilerNamespace is the interface for sharing +-- data between lua scripts and Java +planetiler.output.name = "Power" +planetiler.output.description = "Simple" +planetiler.output.attribution = +'©OpenStreetMap contributors' +planetiler.examples = "power.spec.yaml" +planetiler.output.path = { "data", "power.pmtiles" } + +local area = planetiler.args:get_string("area", "geofabrik area to download", "massachusetts") + +planetiler.add_source('osm', { + type = 'osm', + url = 'geofabrik:' .. area, + -- any java method or field that takes a Path can be called with a list of path parts from lua + path = { 'data', 'sources', area .. '.osm.pbf' } +}) + +function planetiler.process_feature(source, features) + if source:can_be_line() and source:has_tag("power", "line") then + features + :line("power") + :set_min_zoom(7) + :inherit_attr_from_source("power") + :inherit_attr_from_source("voltage") + :inherit_attr_from_source("cables") + :inherit_attr_from_source("operator") + elseif source:isPoint() and source:has_tag("power", "pole") then + features + :point("power") + :set_min_zoom(13) + :inherit_attr_from_source("power") + :inherit_attr_from_source("ref") + :inherit_attr_from_source("height") + :inherit_attr_from_source("operator") + end +end diff --git a/planetiler-experimental/src/test/resources/power.spec.yaml b/planetiler-experimental/src/test/resources/power.spec.yaml new file mode 100644 index 0000000000..556a53fcf8 --- /dev/null +++ b/planetiler-experimental/src/test/resources/power.spec.yaml @@ -0,0 +1,39 @@ +# test cases for power.lua +examples: +- name: power line + input: + geometry: line + tags: + power: line + voltage: voltage + cables: cables + operator: operator + output: + layer: power + geometry: line + min_zoom: 7 + allow_extra_tags: false + tags: + power: line + cables: cables + operator: operator + voltage: voltage + +- name: pole + input: + geometry: point + tags: + power: pole + ref: ref + height: height + operator: operator + output: + layer: power + geometry: point + min_zoom: 13 + allow_extra_tags: false + tags: + power: pole + ref: ref + height: height + operator: operator diff --git a/planetiler-experimental/src/test/resources/roads.lua b/planetiler-experimental/src/test/resources/roads.lua new file mode 100644 index 0000000000..b932027dbc --- /dev/null +++ b/planetiler-experimental/src/test/resources/roads.lua @@ -0,0 +1,50 @@ +-- Simple lua profile example that emits road features + +-- setup archive metadata +planetiler.output.name = "Road Schema" +planetiler.output.description = "Simple" +planetiler.output.attribution = +'©OpenStreetMap contributors' + +-- tell planetiler where the tests are when you run `planetiler.jar validate roads_main.lua` +planetiler.examples = "roads.spec.yaml" + +-- planetiler.process_feature is called by many threads so it can read from shared data structures +-- but not modify them +local highway_minzooms = { + trunk = 5, + primary = 7, + secondary = 8, + tertiary = 9, + motorway_link = 9, + trunk_link = 9, + primary_link = 9, + secondary_link = 9, + tertiary_link = 9, + unclassified = 11, + residential = 11, + living_street = 11, + track = 12, + service = 13 +} + +-- called by planetiler to map each input feature to output vector tile features +function planetiler.process_feature(source, features) + local highway = source:get_tag("highway") + if source:can_be_line() and highway and highway_minzooms[highway] then + features:line("road") + :set_min_zoom(highway_minzooms[highway]) + :set_attr("highway", highway) + end +end + +-- there are 2 ways to invoke planetiler: a main method (see roads_main.lua) and this method that +-- sets up planetiler statically +-- TODO not sure which is better? +local area = planetiler.args:get_string("area", "geofabrik area to download", "germany") +planetiler.add_source('osm', { + type = 'osm', + path = { 'data', 'sources', area .. '.osm.pbf' }, + url = 'geofabrik:' .. area +}) +planetiler.output.path = 'roads.pmtiles' diff --git a/planetiler-experimental/src/test/resources/roads.spec.yaml b/planetiler-experimental/src/test/resources/roads.spec.yaml new file mode 100644 index 0000000000..cb0f46e0a2 --- /dev/null +++ b/planetiler-experimental/src/test/resources/roads.spec.yaml @@ -0,0 +1,41 @@ +# test cases for roads.lua and roads_main.lua +examples: +- name: trunk + input: + geometry: line + tags: + highway: trunk + output: + layer: road + geometry: line + min_zoom: 5 + allow_extra_tags: false + tags: + highway: trunk + +- name: track + input: + geometry: line + tags: + highway: track + output: + layer: road + geometry: line + min_zoom: 12 + allow_extra_tags: false + tags: + highway: track + +- name: service + input: + geometry: line + tags: + highway: service + output: + layer: road + geometry: line + min_zoom: 13 + allow_extra_tags: false + tags: + highway: service + diff --git a/planetiler-experimental/src/test/resources/roads_main.lua b/planetiler-experimental/src/test/resources/roads_main.lua new file mode 100644 index 0000000000..bc36432cf6 --- /dev/null +++ b/planetiler-experimental/src/test/resources/roads_main.lua @@ -0,0 +1,51 @@ +-- Simple lua profile example that emits road features + +-- setup archive metadata +planetiler.output.name = "Road Schema" +planetiler.output.description = "Simple" +planetiler.output.attribution = +'©OpenStreetMap contributors' + +-- tell planetiler where the tests are when you run `planetiler.jar validate roads_main.lua` +planetiler.examples = "roads.spec.yaml" + +-- planetiler.process_feature is called by many threads so it can read from shared data structures +-- but not modify them +local highway_minzooms = { + trunk = 5, + primary = 7, + secondary = 8, + tertiary = 9, + motorway_link = 9, + trunk_link = 9, + primary_link = 9, + secondary_link = 9, + tertiary_link = 9, + unclassified = 11, + residential = 11, + living_street = 11, + track = 12, + service = 13 +} + +-- called by planetiler to map each input feature to output vector tile features +function planetiler.process_feature(source, features) + local highway = source:get_tag("highway") + if source:can_be_line() and highway and highway_minzooms[highway] then + features:line("road") + :set_min_zoom(highway_minzooms[highway]) + :set_attr("highway", highway) + end +end + +-- there are 2 ways to invoke planetiler: a main method that takes a Planetiler instance configured +-- with args and the profile defined above, or setup planetiler with calls to planetiler.add_source +-- and planetiler.output.path. +-- TODO not sure which is better? +function main(runner) + local area = planetiler.args:get_string("area", "geofabrik area to download", "massachusetts") + runner + :add_osm_source("osm", { "data", "sources", area .. ".osm.pbf" }, "geofabrik:" .. area) + :overwrite_output({ "data", "roads.pmtiles" }) + :run() +end diff --git a/pom.xml b/pom.xml index c465c280c3..d55c5b3d40 100644 --- a/pom.xml +++ b/pom.xml @@ -19,6 +19,8 @@ UTF-8 21 21 + + true true 2.16.0 5.10.1 @@ -26,7 +28,9 @@ onthegomap onthegomap_planetiler ${project.artifactId} - planetiler-benchmarks/**/*, planetiler-openmaptiles/**/* + planetiler-benchmarks/**/*, planetiler-openmaptiles/**/*, + planetiler-experimental/src/main/java/org/luaj/**/* + 0.7-SNAPSHOT ${maven.build.timestamp} @@ -87,6 +91,7 @@ planetiler-core planetiler-openmaptiles/submodule.pom.xml planetiler-custommap + planetiler-experimental planetiler-benchmarks planetiler-examples planetiler-dist