From 1e739909948bc95f533dbe5cc556bb2160396c48 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=B3bert=20Papp?= Date: Mon, 27 Nov 2023 22:13:51 +0000 Subject: [PATCH] Migrate to AppEngine gen2 on java17 with Micronaut and Handlebars (#7) --- .github/workflows/CI-build.yml | 1 + AppEngine/README.md | 22 +++ AppEngine/build.gradle | 120 +++++++------ AppEngine/src/main/appengine/app.yaml | 15 ++ .../{webapp/WEB-INF => appengine}/cron.yaml | 0 AppEngine/src/main/appengine/placeholder.txt | 2 + .../neil/plaintext/diff_match_patch.java | 1 + .../net/twisterrob/blt/gapp/Application.java | 17 ++ .../twisterrob/blt/gapp/FeedCronServlet.java | 61 +++++-- .../twisterrob/blt/gapp/IndexController.java | 35 ++++ .../blt/gapp/InternalFeedbackServlet.java | 6 +- .../blt/gapp/LineStatusHistoryServlet.java | 75 ++++++--- .../CustomHandlebarsViewsRenderer.java | 26 +++ .../view/handlebars/HandlebarsHelpers.java | 52 ++++++ .../blt/gapp/viewmodel/Versions.java | 27 +-- AppEngine/src/main/resources/application.yml | 8 + AppEngine/src/main/resources/log4j2.xml | 46 +++++ .../{webapp => resources/public}/favicon.ico | Bin .../public}/static/descript.ion | 0 .../public}/static/htmltooltip.js | 0 .../public}/static/jquery-1.10.2.min.js | 0 .../public}/static/jquery-1.2.2.pack.js | 0 .../public}/static/jquery.min.map | 0 .../src/main/resources/views/LineStatus.hbs | 144 ++++++++++++++++ AppEngine/src/main/resources/views/index.hbs | 33 ++++ AppEngine/src/main/webapp/LineStatus.jsp | 158 ------------------ .../src/main/webapp/WEB-INF/appengine-web.xml | 38 ----- .../WEB-INF/java.util.logging.properties | 30 ---- AppEngine/src/main/webapp/WEB-INF/log4j2.xml | 130 -------------- AppEngine/src/main/webapp/WEB-INF/web.xml | 63 ------- AppEngine/src/main/webapp/index.jsp | 40 ----- .../twisterrob/blt/gapp/HtmlValidator.java | 25 +++ .../blt/gapp/IndexControllerTest.java | 68 ++++++++ .../net/twisterrob/java/io/MailSender.java | 2 +- buildSrc/build.gradle | 3 +- gradle.properties | 23 +-- 36 files changed, 696 insertions(+), 575 deletions(-) create mode 100644 AppEngine/README.md create mode 100644 AppEngine/src/main/appengine/app.yaml rename AppEngine/src/main/{webapp/WEB-INF => appengine}/cron.yaml (100%) create mode 100644 AppEngine/src/main/appengine/placeholder.txt create mode 100644 AppEngine/src/main/java/net/twisterrob/blt/gapp/Application.java create mode 100644 AppEngine/src/main/java/net/twisterrob/blt/gapp/IndexController.java create mode 100644 AppEngine/src/main/java/net/twisterrob/blt/gapp/view/handlebars/CustomHandlebarsViewsRenderer.java create mode 100644 AppEngine/src/main/java/net/twisterrob/blt/gapp/view/handlebars/HandlebarsHelpers.java create mode 100644 AppEngine/src/main/resources/application.yml create mode 100644 AppEngine/src/main/resources/log4j2.xml rename AppEngine/src/main/{webapp => resources/public}/favicon.ico (100%) rename AppEngine/src/main/{webapp => resources/public}/static/descript.ion (100%) rename AppEngine/src/main/{webapp => resources/public}/static/htmltooltip.js (100%) rename AppEngine/src/main/{webapp => resources/public}/static/jquery-1.10.2.min.js (100%) rename AppEngine/src/main/{webapp => resources/public}/static/jquery-1.2.2.pack.js (100%) rename AppEngine/src/main/{webapp => resources/public}/static/jquery.min.map (100%) create mode 100644 AppEngine/src/main/resources/views/LineStatus.hbs create mode 100644 AppEngine/src/main/resources/views/index.hbs delete mode 100644 AppEngine/src/main/webapp/LineStatus.jsp delete mode 100644 AppEngine/src/main/webapp/WEB-INF/appengine-web.xml delete mode 100644 AppEngine/src/main/webapp/WEB-INF/java.util.logging.properties delete mode 100644 AppEngine/src/main/webapp/WEB-INF/log4j2.xml delete mode 100644 AppEngine/src/main/webapp/WEB-INF/web.xml delete mode 100644 AppEngine/src/main/webapp/index.jsp create mode 100644 AppEngine/src/test/java/net/twisterrob/blt/gapp/HtmlValidator.java create mode 100644 AppEngine/src/test/java/net/twisterrob/blt/gapp/IndexControllerTest.java diff --git a/.github/workflows/CI-build.yml b/.github/workflows/CI-build.yml index 4bcda7db..a0277597 100644 --- a/.github/workflows/CI-build.yml +++ b/.github/workflows/CI-build.yml @@ -50,6 +50,7 @@ jobs: --continue --no-build-cache build + :AppEngine:appengineStage - name: "Upload 'Unit Test Results' artifact." if: success() || failure() diff --git a/AppEngine/README.md b/AppEngine/README.md new file mode 100644 index 00000000..a514c2f5 --- /dev/null +++ b/AppEngine/README.md @@ -0,0 +1,22 @@ +## Running a datastore locally + + * Docs: https://cloud.google.com/datastore/docs/tools/datastore-emulator#windows + * Old docs: https://cloud.google.com/appengine/docs/legacy/standard/java/tools/using-local-server#datastore + +1. Install the [Google Cloud SDK](https://cloud.google.com/sdk/). +2. Install the emulator: + ```shell + gcloud components install cloud-datastore-emulator + gcloud components install beta + ``` +3. Run the datastore emulator in the background: + ``` + gcloud --project twisterrob-travel beta emulators datastore start --no-store-on-disk + ``` +4. In a separate window run: + ``` + gcloud beta emulators datastore env-init + ``` +5. Set the output environment variables before running `gradlew :AppEngine:run`. + +Note: step 4 and 5 are automated in `build.gradle`. diff --git a/AppEngine/build.gradle b/AppEngine/build.gradle index 10242220..4d1887d4 100644 --- a/AppEngine/build.gradle +++ b/AppEngine/build.gradle @@ -1,87 +1,109 @@ +import org.gradle.nativeplatform.platform.internal.DefaultNativePlatform + apply plugin: 'java' -apply plugin: 'war' -apply plugin: 'com.google.cloud.tools.appengine-appenginewebxml' -//apply plugin: 'com.google.cloud.tools.endpoints-framework-server' +apply plugin: 'io.micronaut.minimal.application' +apply plugin: 'com.google.cloud.tools.appengine-appyaml' apply plugin: 'idea' java { - sourceCompatibility = JavaVersion.VERSION_1_8 - targetCompatibility = JavaVersion.VERSION_1_8 -} - -configurations { - dev -} -idea { - module { - scopes.PROVIDED.plus += [ configurations.dev ] - } + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 } dependencies { - // com.google.appengine.api.datastore - implementation("com.google.appengine:appengine-api-1.0-sdk:${VERSION_APPENGINE}") - // com.google.api.server.spi.EndpointsServlet - implementation("com.google.endpoints:endpoints-framework:${VERSION_GCLOUD_ENDPOINTS}") + implementation("com.google.appengine:appengine-api-1.0-sdk:2.0.12") + implementation(platform("com.google.cloud:libraries-bom:${VERSION_GCLOUD}")) + implementation("com.google.cloud:google-cloud-datastore") - providedCompile 'javax.servlet:javax.servlet-api:3.1.0' - dev("org.apache.taglibs:taglibs-standard-impl:1.2.5") - dev("javax.servlet.jsp.jstl:jstl-api:1.2") + runtimeOnly("org.yaml:snakeyaml") + runtimeOnly("io.micronaut.serde:micronaut-serde-jackson") + implementation("io.micronaut.views:micronaut-views-handlebars") implementation project(':Shared') - // See java.util.logging.properties and log4j2.xml implementation("org.slf4j:slf4j-api:${VERSION_SLF4J}") - // route apps SLF4J logging to JUL - implementation("org.slf4j:slf4j-jdk14:${VERSION_SLF4J}") - dev("org.slf4j:jul-to-slf4j:${VERSION_SLF4J}") - dev("org.apache.logging.log4j:log4j-api:${VERSION_LOG4J}") - dev("org.apache.logging.log4j:log4j-core:${VERSION_LOG4J}") - dev("org.apache.logging.log4j:log4j-slf4j-impl:${VERSION_LOG4J}") - + runtimeOnly("org.apache.logging.log4j:log4j-slf4j2-impl:${VERSION_LOG4J}") // for org.apache.tools.ant.filters.StringInputStream implementation("ant:ant:1.6.5") apply from: "${rootDir}/gradle/testCompile.gradle", to: project - testImplementation("com.google.appengine:appengine-testing:${VERSION_APPENGINE}") - testImplementation("com.google.appengine:appengine-api-stubs:${VERSION_APPENGINE}") - testImplementation("com.google.appengine:appengine-api-labs:${VERSION_APPENGINE}") + testImplementation("io.micronaut:micronaut-http-client") + testImplementation("com.github.jtidy:jtidy:${VERSION_JTIDY}") } sourceSets { main { java.srcDir 'src/main/diff' - compileClasspath += configurations.dev } test { java.srcDir 'src/test/diff' - compileClasspath += configurations.dev } } -appengine { com.google.cloud.tools.gradle.appengine.standard.AppEngineStandardExtension ext -> +//noinspection UnnecessaryQualifiedReference +appengine { com.google.cloud.tools.gradle.appengine.appyaml.AppEngineAppYamlExtension ext -> + stage { + artifact = file("src/main/appengine/placeholder.txt") + // AppEngine plugin can't handle laziness, wire manually. + tasks.appengineStage.dependsOn(tasks.installDist) + extraFilesDirectories = tasks.installDist + } deploy { + // Live: https://twisterrob-london.appspot.com/ projectId = "twisterrob-london" version = "GCLOUD_CONFIG" - } - run { - port = 8888 - automaticRestart = true - - def logDependencyFilter = { Dependency d -> d.group?.contains('slf4j') || d.group?.contains('log4j') }; - def deps = configurations.dev.dependencies.findAll logDependencyFilter - def flags = configurations.dev.files(deps as Dependency[]) \ - .collect { "-Xbootclasspath/p:${it.absolutePath}".toString() } - jvmFlags = (jvmFlags?: [ ]) + flags + // Test deployment: + //version = "test" + //stopPreviousVersion = false + //promote = false } tools { - cloudSdkHome = System.getenv("GCLOUD_HOME")?: file("build/downloaded/gcloud-sdk") + System.getenv("GCLOUD_HOME")?.tap { cloudSdkHome = it } cloudSdkVersion = VERSION_GCLOUD_SDK as String // https://cloud.google.com/sdk/gcloud/reference#--verbosity verbosity = "info" } } -//endpointsServer { -// hostname = "twisterrob-london.appspot.com" -//} +application { + mainClass.set("net.twisterrob.blt.gapp.Application") +} + +micronaut { + version = VERSION_MICRONAUT as String + runtime("jetty") + testRuntime("junit4") + processing { + incremental(true) + annotations("net.twisterrob.blt.gapp.*") + } +} + +tasks.named("run").configure { JavaExec task -> + if (DefaultNativePlatform.getCurrentOperatingSystem().isWindows()) { + task.systemProperty("log4j.skipJansi", false) + } + if (gradle.startParameter.continuous) { + task.systemProperties( + // TODO use https://docs.micronaut.io/latest/guide/index.html#environments to create overrides for debug. + "micronaut.io.watch.enabled": true, + "micronaut.io.watch.restart": true, + "micronaut.io.watch.paths": "src/main", + ) + } + doFirst { + def stream = new ByteArrayOutputStream() + exec { + commandLine("gcloud.cmd", "beta", "emulators", "datastore", "env-init") + standardOutput = stream + } + Map env = stream.toString().split("\r?\n").collectEntries { line -> + if (!line.startsWith("set ")) { + throw new IllegalStateException("Unexpected line: $line in \n${stream.toString()}") + } + def (key, value) = line.substring(4).split("=", 2) + [ key, value ] + } + task.environment(env) + } +} diff --git a/AppEngine/src/main/appengine/app.yaml b/AppEngine/src/main/appengine/app.yaml new file mode 100644 index 00000000..f2be5d95 --- /dev/null +++ b/AppEngine/src/main/appengine/app.yaml @@ -0,0 +1,15 @@ +# https://cloud.google.com/appengine/docs/standard/reference/app-yaml?tab=java +runtime: java17 +entrypoint: bin/AppEngine + +instance_class: F1 + +automatic_scaling: + min_instances: 0 + max_instances: 1 + +handlers: + - url: /.* + secure: always + redirect_http_response_code: 301 + script: auto diff --git a/AppEngine/src/main/webapp/WEB-INF/cron.yaml b/AppEngine/src/main/appengine/cron.yaml similarity index 100% rename from AppEngine/src/main/webapp/WEB-INF/cron.yaml rename to AppEngine/src/main/appengine/cron.yaml diff --git a/AppEngine/src/main/appengine/placeholder.txt b/AppEngine/src/main/appengine/placeholder.txt new file mode 100644 index 00000000..7883eefc --- /dev/null +++ b/AppEngine/src/main/appengine/placeholder.txt @@ -0,0 +1,2 @@ +This file exists to replace the mandatory "artifact" in AppEngine YAML plugin. +The real files come from the `org.gradle.application` plugin's `installDist` task (build/install/AppEngine). diff --git a/AppEngine/src/main/diff/name/fraser/neil/plaintext/diff_match_patch.java b/AppEngine/src/main/diff/name/fraser/neil/plaintext/diff_match_patch.java index cef385e7..620f47a1 100644 --- a/AppEngine/src/main/diff/name/fraser/neil/plaintext/diff_match_patch.java +++ b/AppEngine/src/main/diff/name/fraser/neil/plaintext/diff_match_patch.java @@ -1794,6 +1794,7 @@ public LinkedList patch_make(LinkedList diffs) { * @return LinkedList of Patch objects. * @deprecated Prefer patch_make(String text1, LinkedList diffs). */ + @Deprecated public LinkedList patch_make(String text1, String text2, LinkedList diffs) { return patch_make(text1, diffs); diff --git a/AppEngine/src/main/java/net/twisterrob/blt/gapp/Application.java b/AppEngine/src/main/java/net/twisterrob/blt/gapp/Application.java new file mode 100644 index 00000000..3e4e759f --- /dev/null +++ b/AppEngine/src/main/java/net/twisterrob/blt/gapp/Application.java @@ -0,0 +1,17 @@ +package net.twisterrob.blt.gapp; + +import io.micronaut.context.ApplicationContext; +import io.micronaut.runtime.Micronaut; + +public class Application { + public static void main(String... args) { + Micronaut micronaut = Micronaut + .build(args) + .classes(Application.class) + .banner(false) + ; + try (ApplicationContext context = micronaut.start()) { + // Nothing yet, wrap in try-with-resources to ensure teardown. + } + } +} diff --git a/AppEngine/src/main/java/net/twisterrob/blt/gapp/FeedCronServlet.java b/AppEngine/src/main/java/net/twisterrob/blt/gapp/FeedCronServlet.java index 9b44f00d..e29600d1 100644 --- a/AppEngine/src/main/java/net/twisterrob/blt/gapp/FeedCronServlet.java +++ b/AppEngine/src/main/java/net/twisterrob/blt/gapp/FeedCronServlet.java @@ -4,12 +4,15 @@ import java.net.*; import java.util.*; -import javax.servlet.http.*; +import io.micronaut.http.annotation.Controller; +import io.micronaut.http.annotation.Get; +import jakarta.servlet.http.*; import org.slf4j.*; -import com.google.appengine.api.datastore.*; -import com.google.appengine.api.datastore.Query.SortDirection; +import com.google.cloud.Timestamp; +import com.google.cloud.datastore.*; +import com.google.cloud.datastore.StructuredQuery.OrderBy; import net.twisterrob.blt.io.feeds.Feed; import net.twisterrob.java.io.IOTools; @@ -17,14 +20,16 @@ import static net.twisterrob.blt.gapp.FeedConsts.*; +@Controller @SuppressWarnings("serial") public class FeedCronServlet extends HttpServlet { private static final Logger LOG = LoggerFactory.getLogger(FeedCronServlet.class); private static final String QUERY_FEED = "feed"; - private final DatastoreService datastore = DatastoreServiceFactory.getDatastoreService(); + private final Datastore datastore = DatastoreOptions.getDefaultInstance().getService(); + @Get("/FeedCron") @Override public void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException { String feedString = String.valueOf(req.getParameter(QUERY_FEED)); Feed feed; @@ -39,7 +44,7 @@ public class FeedCronServlet extends HttpServlet { } Marker marker = MarkerFactory.getMarker(feed.name()); - Entity newEntry = downloadNewEntry(feed); + FullEntity newEntry = downloadNewEntry(datastore, feed); Entity oldEntry = readLatest(datastore, feed); if (oldEntry != null) { if (sameProp(DS_PROP_CONTENT, oldEntry, newEntry)) { @@ -60,29 +65,49 @@ public class FeedCronServlet extends HttpServlet { } } - private static Entity readLatest(DatastoreService datastore, Feed feed) { - // we're only concerned about the latest one, if any - Query q = new Query(feed.name()).addSort(DS_PROP_RETRIEVED_DATE, SortDirection.DESCENDING); - Iterator result = datastore.prepare(q).asIterator(); + private static Entity readLatest(Datastore datastore, Feed feed) { + Query q = Query + .newEntityQueryBuilder() + .setKind(feed.name()) + .addOrderBy(OrderBy.desc(DS_PROP_RETRIEVED_DATE)) + .build() + ; + // We're only concerned about the latest one, if any. + QueryResults result = datastore.run(q); return result.hasNext()? result.next() : null; } - private static boolean sameProp(String propName, Entity oldEntry, Entity newEntry) { - return oldEntry.hasProperty(propName) && newEntry.hasProperty(propName) - && ObjectTools.equals(oldEntry.getProperty(propName), newEntry.getProperty(propName)); + private static boolean sameProp(String propName, BaseEntity oldEntry, BaseEntity newEntry) { + return hasProperty(oldEntry, propName) && hasProperty(newEntry, propName) + && ObjectTools.equals(oldEntry.getValue(propName), newEntry.getValue(propName)); } - public static Entity downloadNewEntry(Feed feed) { - Entity newEntry = new Entity(feed.name()); + static boolean hasProperty(BaseEntity entry, String propName) { + return entry.getProperties().containsKey(propName); + } + + public static FullEntity downloadNewEntry(Datastore datastore, Feed feed) { + KeyFactory keyFactory = datastore.newKeyFactory().setKind(feed.name()); + FullEntity.Builder newEntry = Entity.newBuilder(keyFactory.newKey()); try { String feedResult = downloadFeed(feed); - newEntry.setProperty(DS_PROP_CONTENT, new Text(feedResult)); + newEntry.set(DS_PROP_CONTENT, unindexedString(feedResult)); } catch (Exception ex) { LOG.error("Cannot load '{}'!", feed, ex); - newEntry.setProperty(DS_PROP_ERROR, new Text(ObjectTools.getFullStackTrace(ex))); + newEntry.set(DS_PROP_ERROR, unindexedString(ObjectTools.getFullStackTrace(ex))); } - newEntry.setProperty(DS_PROP_RETRIEVED_DATE, new Date()); - return newEntry; + newEntry.set(DS_PROP_RETRIEVED_DATE, Timestamp.now()); + return newEntry.build(); + } + + /** + * Strings have a limitation of 1500 bytes when indexed. This removes that limitation. + * @see https://cloud.google.com/datastore/docs/concepts/entities#text_string + */ + private static Value unindexedString(String value) { + return value == null + ? NullValue.of() + : StringValue.newBuilder(value).setExcludeFromIndexes(true).build(); } public static String downloadFeed(Feed feed) throws IOException { diff --git a/AppEngine/src/main/java/net/twisterrob/blt/gapp/IndexController.java b/AppEngine/src/main/java/net/twisterrob/blt/gapp/IndexController.java new file mode 100644 index 00000000..6c8192d3 --- /dev/null +++ b/AppEngine/src/main/java/net/twisterrob/blt/gapp/IndexController.java @@ -0,0 +1,35 @@ +package net.twisterrob.blt.gapp; + +import io.micronaut.http.HttpResponse; +import io.micronaut.http.MediaType; +import io.micronaut.http.MutableHttpResponse; +import io.micronaut.http.annotation.Controller; +import io.micronaut.http.annotation.Get; +import io.micronaut.http.server.types.files.StreamedFile; +import io.micronaut.views.View; + +import net.twisterrob.blt.gapp.viewmodel.Versions; + +@Controller +public class IndexController { + + @Get("/") + @View("index") + public MutableHttpResponse index() { + return HttpResponse.ok(new IndexModel(new Versions())); + } + + @Get("/favicon.ico") + public StreamedFile favicon() { + return new StreamedFile( + IndexController.class.getClassLoader().getResourceAsStream("public/favicon.ico"), + MediaType.IMAGE_PNG_TYPE + ); + } + + private record IndexModel( + Versions versions + ) { + + } +} diff --git a/AppEngine/src/main/java/net/twisterrob/blt/gapp/InternalFeedbackServlet.java b/AppEngine/src/main/java/net/twisterrob/blt/gapp/InternalFeedbackServlet.java index 647e17ae..0a561a98 100644 --- a/AppEngine/src/main/java/net/twisterrob/blt/gapp/InternalFeedbackServlet.java +++ b/AppEngine/src/main/java/net/twisterrob/blt/gapp/InternalFeedbackServlet.java @@ -5,16 +5,20 @@ import javax.mail.*; import javax.mail.internet.*; -import javax.servlet.http.*; +import io.micronaut.http.annotation.Controller; +import io.micronaut.http.annotation.Post; +import jakarta.servlet.http.*; import org.slf4j.*; import net.twisterrob.java.io.IOTools; +@Controller @SuppressWarnings("serial") public class InternalFeedbackServlet extends HttpServlet { private static final Logger LOG = LoggerFactory.getLogger(InternalFeedbackServlet.class); + @Post("/InternalFeedback") @Override public void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException { Properties props = new Properties(); Session session = Session.getDefaultInstance(props, null); diff --git a/AppEngine/src/main/java/net/twisterrob/blt/gapp/LineStatusHistoryServlet.java b/AppEngine/src/main/java/net/twisterrob/blt/gapp/LineStatusHistoryServlet.java index 1c332f09..0a6b196d 100644 --- a/AppEngine/src/main/java/net/twisterrob/blt/gapp/LineStatusHistoryServlet.java +++ b/AppEngine/src/main/java/net/twisterrob/blt/gapp/LineStatusHistoryServlet.java @@ -3,13 +3,18 @@ import java.io.*; import java.util.*; -import javax.servlet.*; -import javax.servlet.http.*; +import io.micronaut.http.HttpResponse; +import io.micronaut.http.MediaType; +import io.micronaut.http.annotation.Controller; +import io.micronaut.http.annotation.Get; +import io.micronaut.http.annotation.Produces; +import io.micronaut.views.View; +import jakarta.servlet.http.*; import org.apache.tools.ant.filters.StringInputStream; -import com.google.appengine.api.datastore.*; -import com.google.appengine.api.datastore.Query.SortDirection; +import com.google.cloud.datastore.*; +import com.google.cloud.datastore.StructuredQuery.OrderBy; import net.twisterrob.blt.gapp.viewmodel.*; import net.twisterrob.blt.io.feeds.Feed; @@ -17,9 +22,11 @@ import net.twisterrob.java.utils.ObjectTools; import static net.twisterrob.blt.gapp.FeedConsts.*; +import static net.twisterrob.blt.gapp.FeedCronServlet.hasProperty; +@Controller @SuppressWarnings("serial") -public class LineStatusHistoryServlet extends HttpServlet { +public class LineStatusHistoryServlet { //private static final Logger LOG = LoggerFactory.getLogger(LineStatusHistoryServlet.class); private static final String QUERY_DISPLAY_CURRENT = "current"; @@ -27,9 +34,12 @@ public class LineStatusHistoryServlet extends HttpServlet { private static final String QUERY_DISPLAY_MAX = "max"; private static final int DISPLAY_MAX_DEFAULT = 100; - private final DatastoreService datastore = DatastoreServiceFactory.getDatastoreService(); + private final Datastore datastore = DatastoreOptions.getDefaultInstance().getService(); - @Override public void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + @Get("/LineStatusHistory") + @View("LineStatus") + @Produces(MediaType.TEXT_HTML) + public HttpResponse doGet(HttpServletRequest req, HttpServletResponse resp) { Feed feed = Feed.TubeDepartureBoardsLineStatus; List results = new LinkedList<>(); @@ -41,7 +51,7 @@ public class LineStatusHistoryServlet extends HttpServlet { max = DISPLAY_MAX_DEFAULT; } if (Boolean.parseBoolean(req.getParameter(QUERY_DISPLAY_CURRENT))) { - Entity entry = FeedCronServlet.downloadNewEntry(feed); + FullEntity entry = FeedCronServlet.downloadNewEntry(datastore, feed); results.add(toResult(entry)); } boolean skipErrors = true; @@ -50,8 +60,9 @@ public class LineStatusHistoryServlet extends HttpServlet { } // process them - Iterable entries = fetchEntries(feed); - for (Entity entry : entries) { + Iterator entries = fetchEntries(feed); + while (entries.hasNext()) { + Entity entry = entries.next(); if (--max < 0) { break; // we've had enough } @@ -61,38 +72,52 @@ public class LineStatusHistoryServlet extends HttpServlet { List differences = getDifferences(results, skipErrors); - // display them - req.setAttribute("feedChanges", differences); - req.setAttribute("colors", new LineColor.AllColors(FeedConsts.STATIC_DATA.getLineColors())); - RequestDispatcher view = req.getRequestDispatcher("/LineStatus.jsp"); - view.forward(req, resp); + return HttpResponse.ok( + new LineStatusHistoryModel( + differences, + new LineColor.AllColors(FeedConsts.STATIC_DATA.getLineColors()) + ) + ); } - private static Result toResult(Entity entry) { + private record LineStatusHistoryModel( + List feedChanges, + Iterable colors + ) { + + } + + private static Result toResult(BaseEntity entry) { Result result; - Text content = (Text)entry.getProperty(DS_PROP_CONTENT); - Text error = (Text)entry.getProperty(DS_PROP_ERROR); - Date date = (Date)entry.getProperty(DS_PROP_RETRIEVED_DATE); + String content = hasProperty(entry, DS_PROP_CONTENT) ? entry.getString(DS_PROP_CONTENT) : null; + String error = hasProperty(entry, DS_PROP_ERROR) ? entry.getString(DS_PROP_ERROR) : null; + Date date = entry.getTimestamp(DS_PROP_RETRIEVED_DATE).toDate(); if (content != null) { try { - Feed feed = Feed.valueOf(entry.getKind()); - InputStream stream = new StringInputStream(content.getValue(), ENCODING); + Feed feed = Feed.valueOf(entry.getKey().getKind()); + InputStream stream = new StringInputStream(content, ENCODING); + @SuppressWarnings({"RedundantTypeArguments", "RedundantSuppression"}) // False positive. LineStatusFeed feedContents = feed.getHandler().parse(stream); result = new Result(date, feedContents); } catch (Exception ex) { result = new Result(date, "Error while displaying loaded XML: " + ObjectTools.getFullStackTrace(ex)); } } else if (error != null) { - result = new Result(date, error.getValue()); + result = new Result(date, error); } else { result = new Result(date, "Empty entity"); } return result; } - protected Iterable fetchEntries(Feed feed) { - Query q = new Query(feed.name()).addSort(DS_PROP_RETRIEVED_DATE, SortDirection.DESCENDING); - Iterable results = datastore.prepare(q).asIterable(); + protected Iterator fetchEntries(Feed feed) { + Query q = Query + .newEntityQueryBuilder() + .setKind(feed.name()) + .addOrderBy(OrderBy.desc(DS_PROP_RETRIEVED_DATE)) + .build() + ; + QueryResults results = datastore.run(q); return results; } diff --git a/AppEngine/src/main/java/net/twisterrob/blt/gapp/view/handlebars/CustomHandlebarsViewsRenderer.java b/AppEngine/src/main/java/net/twisterrob/blt/gapp/view/handlebars/CustomHandlebarsViewsRenderer.java new file mode 100644 index 00000000..35f05b79 --- /dev/null +++ b/AppEngine/src/main/java/net/twisterrob/blt/gapp/view/handlebars/CustomHandlebarsViewsRenderer.java @@ -0,0 +1,26 @@ +package net.twisterrob.blt.gapp.view.handlebars; + +import com.github.jknack.handlebars.Handlebars; + +import io.micronaut.context.annotation.Replaces; +import io.micronaut.core.io.scan.ClassPathResourceLoader; +import io.micronaut.views.ViewsConfiguration; +import io.micronaut.views.handlebars.HandlebarsViewsRenderer; +import io.micronaut.views.handlebars.HandlebarsViewsRendererConfiguration; +import jakarta.inject.Singleton; + +@Singleton +@Replaces(HandlebarsViewsRenderer.class) +public final class CustomHandlebarsViewsRenderer extends HandlebarsViewsRenderer { + + public CustomHandlebarsViewsRenderer( + ViewsConfiguration viewsConfiguration, + ClassPathResourceLoader resourceLoader, + HandlebarsViewsRendererConfiguration handlebarsViewsRendererConfiguration, + Handlebars handlebars + ) { + super(viewsConfiguration, resourceLoader, handlebarsViewsRendererConfiguration, handlebars); + + this.handlebars.registerHelpers(HandlebarsHelpers.class); + } +} diff --git a/AppEngine/src/main/java/net/twisterrob/blt/gapp/view/handlebars/HandlebarsHelpers.java b/AppEngine/src/main/java/net/twisterrob/blt/gapp/view/handlebars/HandlebarsHelpers.java new file mode 100644 index 00000000..86f6cfc6 --- /dev/null +++ b/AppEngine/src/main/java/net/twisterrob/blt/gapp/view/handlebars/HandlebarsHelpers.java @@ -0,0 +1,52 @@ +package net.twisterrob.blt.gapp.view.handlebars; + +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.EnumMap; +import java.util.Locale; + +import com.github.jknack.handlebars.Options; + +public class HandlebarsHelpers { + + public static void assign(String varName, Object varValue, Options options) { + options.data(varName, varValue); + } + + public static int add(int base, int increment) { + return base + increment; + } + + public static String concat(String base, String extra) { + return base + extra; + } + + public static boolean not(boolean value) { + return !value; + } + + public static boolean or(boolean left, boolean right) { + return left || right; + } + + public static boolean and(boolean left, boolean right) { + return left && right; + } + + public static boolean empty(String value) { + return value == null || value.isEmpty(); + } + + public static String formatDate(Date value, String pattern) { + return new SimpleDateFormat(pattern, Locale.getDefault()).format(value); + } + + /** + * Workaround for `myEnumMap.[Foo]` and `lookup myEnumMap Foo` not working. + * Usage: Replace {@code (lookup someEnumMap someEnumKey)} with {@code (lookupEnumMap someEnumMap someEnumKey)}. + * @see https://github.com/jknack/handlebars.java/issues/1083 + */ + public static > Object lookupEnumMap(EnumMap map, E key) { + return map.get(key); + } +} diff --git a/AppEngine/src/main/java/net/twisterrob/blt/gapp/viewmodel/Versions.java b/AppEngine/src/main/java/net/twisterrob/blt/gapp/viewmodel/Versions.java index cb36a8e6..ad6a3105 100644 --- a/AppEngine/src/main/java/net/twisterrob/blt/gapp/viewmodel/Versions.java +++ b/AppEngine/src/main/java/net/twisterrob/blt/gapp/viewmodel/Versions.java @@ -1,38 +1,45 @@ package net.twisterrob.blt.gapp.viewmodel; -import com.google.appengine.api.utils.SystemProperty; - +/** + * Last {@code java8} legacy deployment. + *

+ * applicationId: com.google.appengine.application.id=twisterrob-london
+ * applicationVersion: com.google.appengine.application.version=20221107t101946.447701058168130218
+ * environment: com.google.appengine.runtime.environment=Production
+ * version: com.google.appengine.runtime.version=Google App Engine/1.9.98
+ * 
+ */ public class Versions { public String getEnvironmentKey() { - return SystemProperty.environment.key(); + return "NODE_ENV"; } public String getEnvironment() { - return SystemProperty.environment.get(); + return System.getenv(getEnvironmentKey()); } public String getVersionKey() { - return SystemProperty.version.key(); + return "GAE_DEPLOYMENT_ID"; } public String getVersion() { - return SystemProperty.version.get(); + return System.getenv(getVersionKey()); } public String getApplicationIdKey() { - return SystemProperty.applicationId.key(); + return "GAE_APPLICATION"; } public String getApplicationId() { - return SystemProperty.applicationId.get(); + return System.getenv(getApplicationIdKey()); } public String getApplicationVersionKey() { - return SystemProperty.applicationVersion.key(); + return "GAE_VERSION"; } public String getApplicationVersion() { - return SystemProperty.applicationVersion.get(); + return System.getenv(getApplicationVersionKey()); } } diff --git a/AppEngine/src/main/resources/application.yml b/AppEngine/src/main/resources/application.yml new file mode 100644 index 00000000..4eb485df --- /dev/null +++ b/AppEngine/src/main/resources/application.yml @@ -0,0 +1,8 @@ +micronaut: + server: + port: ${PORT:8080} + router: + static-resources: + default: + mapping: "/static/**" + paths: classpath:public/static diff --git a/AppEngine/src/main/resources/log4j2.xml b/AppEngine/src/main/resources/log4j2.xml new file mode 100644 index 00000000..f0e9fc35 --- /dev/null +++ b/AppEngine/src/main/resources/log4j2.xml @@ -0,0 +1,46 @@ + + + + %highlight{%date{ISO8601} %-5level [%thread] %message%n%throwable + + + + FATAL=bright magenta, ERROR=bright red, WARN=bright yellow, INFO=dim white, DEBUG=green, TRACE=cyan + + + %highlight{%date{ISO8601} %-5level [%thread] %logger(%file:%line)}{ + ${pattern_level_colors}} %style{%message}{bright white}%n%style{%throwable}{BG_red white} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/AppEngine/src/main/webapp/favicon.ico b/AppEngine/src/main/resources/public/favicon.ico similarity index 100% rename from AppEngine/src/main/webapp/favicon.ico rename to AppEngine/src/main/resources/public/favicon.ico diff --git a/AppEngine/src/main/webapp/static/descript.ion b/AppEngine/src/main/resources/public/static/descript.ion similarity index 100% rename from AppEngine/src/main/webapp/static/descript.ion rename to AppEngine/src/main/resources/public/static/descript.ion diff --git a/AppEngine/src/main/webapp/static/htmltooltip.js b/AppEngine/src/main/resources/public/static/htmltooltip.js similarity index 100% rename from AppEngine/src/main/webapp/static/htmltooltip.js rename to AppEngine/src/main/resources/public/static/htmltooltip.js diff --git a/AppEngine/src/main/webapp/static/jquery-1.10.2.min.js b/AppEngine/src/main/resources/public/static/jquery-1.10.2.min.js similarity index 100% rename from AppEngine/src/main/webapp/static/jquery-1.10.2.min.js rename to AppEngine/src/main/resources/public/static/jquery-1.10.2.min.js diff --git a/AppEngine/src/main/webapp/static/jquery-1.2.2.pack.js b/AppEngine/src/main/resources/public/static/jquery-1.2.2.pack.js similarity index 100% rename from AppEngine/src/main/webapp/static/jquery-1.2.2.pack.js rename to AppEngine/src/main/resources/public/static/jquery-1.2.2.pack.js diff --git a/AppEngine/src/main/webapp/static/jquery.min.map b/AppEngine/src/main/resources/public/static/jquery.min.map similarity index 100% rename from AppEngine/src/main/webapp/static/jquery.min.map rename to AppEngine/src/main/resources/public/static/jquery.min.map diff --git a/AppEngine/src/main/resources/views/LineStatus.hbs b/AppEngine/src/main/resources/views/LineStatus.hbs new file mode 100644 index 00000000..229f5eff --- /dev/null +++ b/AppEngine/src/main/resources/views/LineStatus.hbs @@ -0,0 +1,144 @@ + + + + Line Status + + + + + + +{{#each feedChanges as | feedChange | }} + {{assign "feed" feedChange.current}} + + + + + + + + + + + + + {{#each feed.content.lineStatuses as | lineStatus | }} + {{assign "changeStatus" (lookupEnumMap feedChange.statuses lineStatus.line)}} + {{assign "changeDescription" (lookupEnumMap feedChange.descriptions lineStatus.line)}} + {{assign "delayStyle" ""}} + {{#if (not (empty lineStatus.description))}} + {{assign "delayStyle" (concat delayStyle " hasDetails")}} + {{/if}} + {{#if (not lineStatus.active)}} + {{assign "delayStyle" (concat delayStyle " inactive")}} + {{/if}} + + + + + + {{/each}} + + + + + + +
+ {{add @index 1}}/{{../feedChanges.length}}: + {{formatDate feed.when "yyyy-MM-dd HH:mm:ss"}} +
LineDisruption TypeChange
{{lineStatus.line.title}} + {{#if (or (empty lineStatus.description) (empty lineStatus.branchDescription))}} + {{lineStatus.type.title}} + {{else}} + {{lineStatus.type.title}} +
+

{{lineStatus.description}}

+

{{lineStatus.branchDescription}}

+
+ {{/if}} +
+ {{#if (empty changeDescription)}} + {{changeStatus.title}} + {{else}} + {{changeStatus.title}} +
{{{changeDescription}}}
+ {{/if}} +
+ {{#if (not (empty feed.errorHeader))}} + {{feedChange.error.title}}: {{feed.errorHeader}} +
+
{{feed.fullError}}
+
+ {{else if (not (empty feedChange.error.title))}} + {{feedChange.error.title}} + {{else}} + {{!-- Make sure there's always content, otherwise the height of the tables is not the same --}} +   + {{/if}} +
+{{/each}} + + diff --git a/AppEngine/src/main/resources/views/index.hbs b/AppEngine/src/main/resources/views/index.hbs new file mode 100644 index 00000000..236d7355 --- /dev/null +++ b/AppEngine/src/main/resources/views/index.hbs @@ -0,0 +1,33 @@ + + + + + + + Twister's Better London Travel Home + + + +

Twister's Better London Travel Home

+ +

Available apps

+ + +

Version info

+
+
applicationId
+
{{versions.applicationIdKey}}={{versions.applicationId}}
+
applicationVersion
+
{{versions.applicationVersionKey}}={{versions.applicationVersion}}
+
environment
+
{{versions.environmentKey}}={{versions.environment}}
+
version
+
{{versions.versionKey}}={{versions.version}}
+
+ + + diff --git a/AppEngine/src/main/webapp/LineStatus.jsp b/AppEngine/src/main/webapp/LineStatus.jsp deleted file mode 100644 index c5aaf321..00000000 --- a/AppEngine/src/main/webapp/LineStatus.jsp +++ /dev/null @@ -1,158 +0,0 @@ -<%@ page contentType="text/html" pageEncoding="UTF-8"%> -<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%> -<%@ taglib prefix="fn" uri="http://java.sun.com/jsp/jstl/functions"%> -<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt"%> - - - - - - Line Status - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- ${status.index+1}/${fn:length(feedChanges)}: - -
LineDisruption TypeChange
${lineStatus.line.title} - - - ${lineStatus.type.title} - - - ${lineStatus.type.title} -
-

${lineStatus.description}

-

${lineStatus.branchDescription}

-
-
-
-
- - - ${feedChange.statuses[lineStatus.line].title} - - - ${feedChange.statuses[lineStatus.line].title} -
${feedChange.descriptions[lineStatus.line]}
-
-
-
- - - ${feedChange.error.title}: ${feed.errorHeader} -
-
${feed.fullError}
-
-
- - ${feedChange.error.title} - - <%-- Make sure there's always content, otherwise the height of the tables is not the same --%> - -   - -
-
-
- - diff --git a/AppEngine/src/main/webapp/WEB-INF/appengine-web.xml b/AppEngine/src/main/webapp/WEB-INF/appengine-web.xml deleted file mode 100644 index 51b865e7..00000000 --- a/AppEngine/src/main/webapp/WEB-INF/appengine-web.xml +++ /dev/null @@ -1,38 +0,0 @@ - - - - twisterrob-london - live - java8 - - - true - - F1 - - 0 - 1 - - - - - - - - - - - - - - true - - - - diff --git a/AppEngine/src/main/webapp/WEB-INF/java.util.logging.properties b/AppEngine/src/main/webapp/WEB-INF/java.util.logging.properties deleted file mode 100644 index 22f71a47..00000000 --- a/AppEngine/src/main/webapp/WEB-INF/java.util.logging.properties +++ /dev/null @@ -1,30 +0,0 @@ -# A default java.util.logging configuration. (All App Engine logging is through java.util.logging by default). -# To use this configuration, WEB-INF/appengine-web.xml references this file (java.util.logging.config.file system property). - -# Set the default logging level for all loggers to WARNING -# See possible levels at java.util.logging.Level: -# http://docs.oracle.com/javase/6/docs/api/java/util/logging/Level.html -# OFF, SEVERE, WARNING, INFO, CONFIG, FINE, FINER, FINEST, ALL -.level=WARNING -net.twisterrob.level=INFO - -# FINER Exception getting module instance @ (DevAppServerModulesFilter.java:212) -# com.google.appengine.api.labs.modules.ModulesException: No valid instance id for this instance. -#com.google.appengine.tools.development.DevAppServerModulesFilter.level = CONFIG -# Route JDK JUL logging through SLF4J to Log4J -# JUL config enables full logging and sets up a bridge that handles JUL -> SLF4J routing -# LogJ4 config receives all logs and filters them according to the configuration -# App uses SLF4J so that just directly routes to Log4J in DEV and JUL in PROD -# PROD routes app's SLF4J to JUL through Gradle compile dependency which is ignored in DEV. -# For this to work the following jars must be on the bootstrap classpath in DEV -# (edit launch configuration classpath): -# * jul-to-slf4j -# * slf4j-api -# * slf4j-log4j12 -# * log4j -# See log4j2.xml for more info on DEV setup. - -# otherwise you'll get the following error: -# Can't load log handler "org.slf4j.bridge.SLF4JBridgeHandler" -# java.lang.ClassNotFoundException: org.slf4j.bridge.SLF4JBridgeHandler -handlers=org.slf4j.bridge.SLF4JBridgeHandler diff --git a/AppEngine/src/main/webapp/WEB-INF/log4j2.xml b/AppEngine/src/main/webapp/WEB-INF/log4j2.xml deleted file mode 100644 index 28aed0c7..00000000 --- a/AppEngine/src/main/webapp/WEB-INF/log4j2.xml +++ /dev/null @@ -1,130 +0,0 @@ - - - - - %highlight{%date{ISO8601} %-5level [%thread] %message%n%throwable - - - - - FATAL=bright magenta, ERROR=bright red, WARN=bright yellow, INFO=dim white, DEBUG=green, TRACE=cyan - - - %highlight{%date{ISO8601} %-5level [%thread] %logger(%file:%line)}{ - ${pattern_level_colors}} %style{%message}{bright,white}%n%style{%throwable}{BG_red,white} - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/AppEngine/src/main/webapp/WEB-INF/web.xml b/AppEngine/src/main/webapp/WEB-INF/web.xml deleted file mode 100644 index 09bc441d..00000000 --- a/AppEngine/src/main/webapp/WEB-INF/web.xml +++ /dev/null @@ -1,63 +0,0 @@ - - - - - - - LineStatusHistory - net.twisterrob.blt.gapp.LineStatusHistoryServlet - - - LineStatusHistory - /LineStatusHistory - - - - FeedCron - net.twisterrob.blt.gapp.FeedCronServlet - - - FeedCron - /FeedCron - - - - InternalFeedback - net.twisterrob.blt.gapp.InternalFeedbackServlet - - - InternalFeedback - /InternalFeedback - - - - EndpointsServlet - com.google.api.server.spi.EndpointsServlet - - services - - - - - EndpointsServlet - /_ah/api/* - - - - - index.jsp - - - - - *.jsp - true - - - diff --git a/AppEngine/src/main/webapp/index.jsp b/AppEngine/src/main/webapp/index.jsp deleted file mode 100644 index fd756f52..00000000 --- a/AppEngine/src/main/webapp/index.jsp +++ /dev/null @@ -1,40 +0,0 @@ -<%@ page contentType="text/html" pageEncoding="UTF-8" %> -<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> -<%@ taglib prefix="fn" uri="http://java.sun.com/jsp/jstl/functions" %> -<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %> - - - - - - - - Twister's Better London Travel Home - - - -

Twister's Better London Travel Home

- -

Available apps

- - -

Version info

-
-
applicationId
-
${versions.applicationIdKey}=${versions.applicationId}
-
applicationVersion
-
${versions.applicationVersionKey}=${versions.applicationVersion}
-
environment
-
${versions.environmentKey}=${versions.environment}
-
version
-
${versions.versionKey}=${versions.version}
-
- - - - - diff --git a/AppEngine/src/test/java/net/twisterrob/blt/gapp/HtmlValidator.java b/AppEngine/src/test/java/net/twisterrob/blt/gapp/HtmlValidator.java new file mode 100644 index 00000000..d1a8210b --- /dev/null +++ b/AppEngine/src/test/java/net/twisterrob/blt/gapp/HtmlValidator.java @@ -0,0 +1,25 @@ +package net.twisterrob.blt.gapp; + +import java.io.StringReader; +import java.io.StringWriter; +import java.util.ArrayList; +import java.util.List; + +import org.w3c.tidy.Tidy; +import org.w3c.tidy.TidyMessage; + +import static org.junit.Assert.assertEquals; + +public class HtmlValidator { + + public static void assertValidHtml(String html) { + Tidy tidy = new Tidy(); + tidy.setQuiet(true); + tidy.setShowWarnings(true); + + List messages = new ArrayList<>(); + tidy.setMessageListener(messages::add); + tidy.parse(new StringReader(html), new StringWriter()); + assertEquals(html + "\n" + messages, 0, messages.size()); + } +} diff --git a/AppEngine/src/test/java/net/twisterrob/blt/gapp/IndexControllerTest.java b/AppEngine/src/test/java/net/twisterrob/blt/gapp/IndexControllerTest.java new file mode 100644 index 00000000..c4c65dd8 --- /dev/null +++ b/AppEngine/src/test/java/net/twisterrob/blt/gapp/IndexControllerTest.java @@ -0,0 +1,68 @@ +package net.twisterrob.blt.gapp; + +import java.io.IOException; + +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.Test; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +import io.micronaut.context.ApplicationContext; +import io.micronaut.http.HttpRequest; +import io.micronaut.http.client.HttpClient; +import io.micronaut.runtime.server.EmbeddedServer; + +import net.twisterrob.java.io.IOTools; + +import static net.twisterrob.blt.gapp.HtmlValidator.assertValidHtml; + +// TODO use @MicronautTest in JUnit 5. +public class IndexControllerTest { + + private static EmbeddedServer server; + private static HttpClient client; + + @BeforeClass + public static void setupServer() { + server = ApplicationContext + .run(EmbeddedServer.class); + client = server + .getApplicationContext() + .createBean(HttpClient.class, server.getURL()); + } + + @AfterClass + public static void stopServer() { + if (server != null) { + server.stop(); + } + if (client != null) { + client.stop(); + } + } + + @Test + public void testIndex() { + HttpRequest request = HttpRequest.GET("/"); + + String body = client.toBlocking().retrieve(request); + + assertNotNull(body); + assertTrue(body.contains("Better London Travel")); + assertValidHtml(body); + } + + @Test + public void testFavicon() throws IOException { + HttpRequest request = HttpRequest.GET("/favicon.ico"); + + byte[] body = client.toBlocking().retrieve(request, byte[].class); + + assertNotNull(body); + byte[] expected = IOTools.readBytes(IndexController.class.getResourceAsStream("/public/favicon.ico")); + assertArrayEquals(expected, body); + } +} diff --git a/Shared/src/main/java/net/twisterrob/java/io/MailSender.java b/Shared/src/main/java/net/twisterrob/java/io/MailSender.java index e8f55c9e..d41c256d 100644 --- a/Shared/src/main/java/net/twisterrob/java/io/MailSender.java +++ b/Shared/src/main/java/net/twisterrob/java/io/MailSender.java @@ -14,7 +14,7 @@ public class MailSender { public void send() throws IOException { try { - URL url = new URL("http://twisterrob-london.appspot.com/InternalFeedback"); + URL url = new URL("https://twisterrob-london.appspot.com/InternalFeedback"); HttpURLConnection conn = (HttpURLConnection)url.openConnection(); conn.setDoInput(true); conn.setDoOutput(true); diff --git a/buildSrc/build.gradle b/buildSrc/build.gradle index 1a829b10..1b063d7f 100644 --- a/buildSrc/build.gradle +++ b/buildSrc/build.gradle @@ -1,6 +1,7 @@ repositories { google() mavenCentral() + gradlePluginPortal() } def props = new Properties() @@ -12,6 +13,6 @@ dependencies { implementation "com.android.tools.build:gradle:${props.VERSION_AGP}" // https://github.com/GoogleCloudPlatform/app-gradle-plugin implementation "com.google.cloud.tools:appengine-gradle-plugin:${props.VERSION_APPENGINE_PLUGIN}" - implementation "com.google.cloud.tools:endpoints-framework-gradle-plugin:${props.VERSION_GCLOUD_ENDPOINTS_PLUGIN}" + implementation "io.micronaut.application:io.micronaut.application.gradle.plugin:${props.VERSION_MICRONAUT}" implementation "org.gretty:gretty:3.0.5" } diff --git a/gradle.properties b/gradle.properties index 1592ce0c..29592d5b 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,7 +1,7 @@ org.gradle.daemon=true -org.gradle.configureondemand=true org.gradle.parallel=true -org.gradle.jvmargs=-Xmx2048M +org.gradle.jvmargs=-Xmx2048M -Dorg.gradle.deprecation.trace=true +org.gradle.warning.mode=fail android.useAndroidX=true android.enableJetifier=true @@ -11,7 +11,7 @@ android.nonTransitiveRClass=false # # Common # -VERSION_SLF4J=1.7.30 +VERSION_SLF4J=2.0.9 VERSION_JSR305=3.0.2 # # @@ -31,17 +31,17 @@ VERSION_GLIDE=3.8.0 # https://github.com/GoogleCloudPlatform/app-gradle-plugin/blob/master/CHANGELOG.md VERSION_APPENGINE_PLUGIN=2.5.0 # https://cloud.google.com/appengine/docs/standard/java/release-notes -VERSION_APPENGINE=1.9.83 -# https://github.com/GoogleCloudPlatform/endpoints-framework-gradle-plugin/blob/master/CHANGELOG.md -VERSION_GCLOUD_ENDPOINTS_PLUGIN=2.1.0 -# https://mvnrepository.com/artifact/com.google.endpoints/endpoints-framework -VERSION_GCLOUD_ENDPOINTS=2.2.2 +VERSION_APPENGINE=1.9.98 # Run `gcloud components update` to get the new available version. # (or `gcloud components update --version ` to revert) # https://cloud.google.com/sdk/docs/release-notes -VERSION_GCLOUD_SDK=347.0.0 -# https://logging.apache.org/log4j/2.x/changes-report.html -VERSION_LOG4J=2.14.0 +VERSION_GCLOUD_SDK=455.0.0 +VERSION_GCLOUD=26.27.0 + +# +VERSION_MICRONAUT=4.2.0 +# https://logging.apache.org/log4j/2.x/release-notes.html +VERSION_LOG4J=2.22.0 # # # Test @@ -51,3 +51,4 @@ VERSION_JUNIT=4.13.2 # Use this instead of 1.3 # If `hamcrest-1.3` appears in the dependency list, check if it's excluded from all usages. VERSION_HAMCREST=2.0.0.0 +VERSION_JTIDY=1.0.5