diff --git a/deployment/src/main/java/io/quarkiverse/quinoa/deployment/ForwardedDevProcessor.java b/deployment/src/main/java/io/quarkiverse/quinoa/deployment/ForwardedDevProcessor.java index 9dec3de6..0be94ba5 100644 --- a/deployment/src/main/java/io/quarkiverse/quinoa/deployment/ForwardedDevProcessor.java +++ b/deployment/src/main/java/io/quarkiverse/quinoa/deployment/ForwardedDevProcessor.java @@ -3,7 +3,7 @@ import static io.quarkiverse.quinoa.QuinoaRecorder.QUINOA_ROUTE_ORDER; import static io.quarkiverse.quinoa.QuinoaRecorder.QUINOA_SPA_ROUTE_ORDER; import static io.quarkiverse.quinoa.deployment.config.QuinoaConfig.isDevServerMode; -import static io.quarkiverse.quinoa.deployment.config.QuinoaConfig.toHandlerConfig; +import static io.quarkiverse.quinoa.deployment.config.QuinoaConfig.toDevProxyHandlerConfig; import static io.quarkiverse.quinoa.deployment.packagemanager.PackageManagerRunner.DEV_PROCESS_THREAD_PREDICATE; import static io.quarkus.deployment.annotations.ExecutionTime.RUNTIME_INIT; import static io.quarkus.deployment.dev.testing.MessageFormat.RESET; @@ -28,7 +28,7 @@ import org.jboss.logging.Logger; -import io.quarkiverse.quinoa.QuinoaHandlerConfig; +import io.quarkiverse.quinoa.QuinoaDevProxyHandlerConfig; import io.quarkiverse.quinoa.QuinoaRecorder; import io.quarkiverse.quinoa.deployment.config.DevServerConfig; import io.quarkiverse.quinoa.deployment.config.QuinoaConfig; @@ -204,7 +204,7 @@ public void runtimeInit( return; } LOG.infof("Quinoa is forwarding unhandled requests to port: %d", devProxy.get().getPort()); - final QuinoaHandlerConfig handlerConfig = toHandlerConfig(quinoaConfig, true, httpBuildTimeConfig); + final QuinoaDevProxyHandlerConfig handlerConfig = toDevProxyHandlerConfig(quinoaConfig, httpBuildTimeConfig); routes.produce(RouteBuildItem.builder().orderedRoute("/*", QUINOA_ROUTE_ORDER) .handler(recorder.quinoaProxyDevHandler(handlerConfig, vertx.getVertx(), devProxy.get().getHost(), devProxy.get().getPort(), @@ -216,7 +216,7 @@ public void runtimeInit( if (quinoaConfig.enableSPARouting()) { resumeOn404.produce(new ResumeOn404BuildItem()); routes.produce(RouteBuildItem.builder().orderedRoute("/*", QUINOA_SPA_ROUTE_ORDER) - .handler(recorder.quinoaSPARoutingHandler(handlerConfig)) + .handler(recorder.quinoaSPARoutingHandler(handlerConfig.ignoredPathPrefixes)) .build()); } } diff --git a/deployment/src/main/java/io/quarkiverse/quinoa/deployment/QuinoaProcessor.java b/deployment/src/main/java/io/quarkiverse/quinoa/deployment/QuinoaProcessor.java index abc6cfe6..a933b63d 100644 --- a/deployment/src/main/java/io/quarkiverse/quinoa/deployment/QuinoaProcessor.java +++ b/deployment/src/main/java/io/quarkiverse/quinoa/deployment/QuinoaProcessor.java @@ -1,11 +1,7 @@ package io.quarkiverse.quinoa.deployment; -import static io.quarkiverse.quinoa.QuinoaRecorder.META_INF_WEB_UI; -import static io.quarkiverse.quinoa.QuinoaRecorder.QUINOA_ROUTE_ORDER; import static io.quarkiverse.quinoa.QuinoaRecorder.QUINOA_SPA_ROUTE_ORDER; -import static io.quarkiverse.quinoa.deployment.config.QuinoaConfig.isDevServerMode; -import static io.quarkiverse.quinoa.deployment.config.QuinoaConfig.isEnabled; -import static io.quarkiverse.quinoa.deployment.config.QuinoaConfig.toHandlerConfig; +import static io.quarkiverse.quinoa.deployment.config.QuinoaConfig.*; import static io.quarkiverse.quinoa.deployment.framework.FrameworkType.overrideConfig; import static io.quarkiverse.quinoa.deployment.packagemanager.PackageManagerRunner.autoDetectPackageManager; import static io.quarkus.deployment.annotations.ExecutionTime.RUNTIME_INIT; @@ -30,7 +26,6 @@ import org.jboss.logging.Logger; -import io.quarkiverse.quinoa.QuinoaHandlerConfig; import io.quarkiverse.quinoa.QuinoaRecorder; import io.quarkiverse.quinoa.deployment.config.QuinoaConfig; import io.quarkiverse.quinoa.deployment.framework.FrameworkType; @@ -42,23 +37,19 @@ import io.quarkiverse.quinoa.deployment.packagemanager.PackageManagerRunner; import io.quarkiverse.quinoa.deployment.packagemanager.types.PackageManagerType; import io.quarkus.deployment.IsDevelopment; -import io.quarkus.deployment.IsNormal; import io.quarkus.deployment.annotations.BuildProducer; import io.quarkus.deployment.annotations.BuildStep; import io.quarkus.deployment.annotations.Record; import io.quarkus.deployment.builditem.FeatureBuildItem; -import io.quarkus.deployment.builditem.GeneratedResourceBuildItem; import io.quarkus.deployment.builditem.HotDeploymentWatchedFileBuildItem; import io.quarkus.deployment.builditem.LaunchModeBuildItem; import io.quarkus.deployment.builditem.LiveReloadBuildItem; -import io.quarkus.deployment.builditem.nativeimage.NativeImageResourceBuildItem; import io.quarkus.deployment.pkg.builditem.OutputTargetBuildItem; import io.quarkus.deployment.util.FileUtil; -import io.quarkus.resteasy.reactive.server.spi.ResumeOn404BuildItem; import io.quarkus.runtime.LaunchMode; import io.quarkus.runtime.configuration.ConfigurationException; import io.quarkus.vertx.http.deployment.RouteBuildItem; -import io.quarkus.vertx.http.runtime.HttpBuildTimeConfig; +import io.quarkus.vertx.http.deployment.spi.GeneratedStaticResourceBuildItem; public class QuinoaProcessor { @@ -198,28 +189,12 @@ && isDevServerMode(configuredQuinoa.resolvedConfig())) { return new TargetDirBuildItem(targetBuildDir); } - @BuildStep(onlyIf = IsNormal.class) - public BuiltResourcesBuildItem prepareResourcesForNormalMode( - Optional targetDir, - BuildProducer generatedResources, - BuildProducer nativeImageResources) throws IOException { - if (targetDir.isEmpty()) { - return null; - } - return new BuiltResourcesBuildItem( - prepareBuiltResources(generatedResources, nativeImageResources, targetDir.get().getBuildDirectory())); - } - - @BuildStep(onlyIfNot = IsNormal.class) - public BuiltResourcesBuildItem prepareResourcesForOtherMode( - Optional targetDir, - BuildProducer generatedResources) throws IOException { + @BuildStep + public BuiltResourcesBuildItem prepareBuiltResources(Optional targetDir) throws IOException { if (targetDir.isEmpty()) { return null; } - final HashSet entries = prepareBuiltResources(generatedResources, - null, targetDir.get().getBuildDirectory()); - return new BuiltResourcesBuildItem(targetDir.get().getBuildDirectory(), entries); + return new BuiltResourcesBuildItem(lookupBuiltResources(targetDir.get().getBuildDirectory())); } @BuildStep(onlyIf = IsDevelopment.class) @@ -240,60 +215,56 @@ void watchChanges( scan(quinoaDir.get().uiDir(), quinoaDir.get().uiDir(), watchedPaths); } + @BuildStep + public void produceGeneratedStaticResources( + ConfiguredQuinoaBuildItem configuredQuinoa, + BuildProducer generatedStaticResourceProducer, + Optional uiResources) throws IOException { + if (configuredQuinoa != null && configuredQuinoa.resolvedConfig().justBuild()) { + LOG.info("Quinoa is in build only mode"); + return; + } + if (uiResources.isPresent() && !uiResources.get().resources().isEmpty()) { + for (BuiltResourcesBuildItem.BuiltResource resource : uiResources.get().resources()) { + generatedStaticResourceProducer + .produce(new GeneratedStaticResourceBuildItem(resource.name(), resource.content())); + } + } + } + @BuildStep @Record(RUNTIME_INIT) public void runtimeInit( ConfiguredQuinoaBuildItem configuredQuinoa, - HttpBuildTimeConfig httpBuildTimeConfig, - LaunchModeBuildItem launchMode, - Optional uiResources, QuinoaRecorder recorder, BuildProducer routes, - BuildProducer resumeOn404) throws IOException { + Optional uiResources) throws IOException { if (configuredQuinoa != null && configuredQuinoa.resolvedConfig().justBuild()) { - LOG.info("Quinoa is in build only mode"); return; } - if (uiResources.isPresent() && !uiResources.get().getNames().isEmpty()) { - String directory = null; - if (uiResources.get().getDirectory().isPresent()) { - directory = uiResources.get().getDirectory().get().toAbsolutePath().toString(); - } - final QuinoaHandlerConfig handlerConfig = toHandlerConfig(configuredQuinoa.resolvedConfig(), - launchMode.getLaunchMode() == LaunchMode.DEVELOPMENT, - httpBuildTimeConfig); - resumeOn404.produce(new ResumeOn404BuildItem()); - routes.produce(RouteBuildItem.builder().orderedRoute("/*", QUINOA_ROUTE_ORDER) - .handler(recorder.quinoaHandler(handlerConfig, directory, - uiResources.get().getNames())) - .build()); + if (uiResources.isPresent() && !uiResources.get().resources().isEmpty()) { if (configuredQuinoa.resolvedConfig().enableSPARouting()) { routes.produce(RouteBuildItem.builder().orderedRoute("/*", QUINOA_SPA_ROUTE_ORDER) - .handler(recorder.quinoaSPARoutingHandler(handlerConfig)) + .handler(recorder + .quinoaSPARoutingHandler(getNormalizedIgnoredPathPrefixes(configuredQuinoa.resolvedConfig()))) .build()); } } } - private HashSet prepareBuiltResources( - BuildProducer generatedResources, - BuildProducer nativeImageResources, - Path targetDir) throws IOException { - final List files = Files.walk(targetDir, FileVisitOption.FOLLOW_LINKS).filter(Files::isRegularFile) - .collect(Collectors.toList()); - final HashSet entries = new HashSet<>(files.size()); - LOG.infof("Quinoa target directory: '%s'", targetDir); - for (Path file : files) { - final String name = "/" + targetDir.relativize(file).toString().replace('\\', '/'); - LOG.infof("Quinoa generated resource: '%s'", name); - generatedResources.produce(new GeneratedResourceBuildItem(META_INF_WEB_UI + name, Files.readAllBytes(file), true)); - if (nativeImageResources != null) { - nativeImageResources - .produce(new NativeImageResourceBuildItem(META_INF_WEB_UI + name)); + private HashSet lookupBuiltResources(Path targetDir) throws IOException { + try (Stream paths = Files.walk(targetDir, FileVisitOption.FOLLOW_LINKS).filter(Files::isRegularFile)) { + final var files = paths.toList(); + final HashSet entries = new HashSet<>(files.size()); + LOG.infof("Quinoa target directory: '%s'", targetDir); + for (Path file : files) { + final String name = "/" + targetDir.relativize(file).toString().replace('\\', '/'); + LOG.infof("Quinoa generated resource: '%s'", name); + entries.add(new BuiltResourcesBuildItem.BuiltResource(name, Files.readAllBytes(file))); } - entries.add(new BuiltResourcesBuildItem.BuiltResource(name)); + return entries; } - return entries; + } private void scan(Path uiDir, Path directory, BuildProducer watchedPaths) @@ -454,4 +425,4 @@ public Path getUIDir() { } } -} \ No newline at end of file +} diff --git a/deployment/src/main/java/io/quarkiverse/quinoa/deployment/config/DevServerConfig.java b/deployment/src/main/java/io/quarkiverse/quinoa/deployment/config/DevServerConfig.java index 1bb75901..9a86dc78 100644 --- a/deployment/src/main/java/io/quarkiverse/quinoa/deployment/config/DevServerConfig.java +++ b/deployment/src/main/java/io/quarkiverse/quinoa/deployment/config/DevServerConfig.java @@ -83,7 +83,7 @@ public interface DevServerConfig { /** * Set this value if the index page is different for the dev-server */ - @ConfigDocDefault("auto-detected falling back to the quinoa.index-page") + @ConfigDocDefault("auto-detected falling back to index.html") Optional indexPage(); /** diff --git a/deployment/src/main/java/io/quarkiverse/quinoa/deployment/config/QuinoaConfig.java b/deployment/src/main/java/io/quarkiverse/quinoa/deployment/config/QuinoaConfig.java index d9859bc1..0d0c0431 100644 --- a/deployment/src/main/java/io/quarkiverse/quinoa/deployment/config/QuinoaConfig.java +++ b/deployment/src/main/java/io/quarkiverse/quinoa/deployment/config/QuinoaConfig.java @@ -11,7 +11,7 @@ import org.eclipse.microprofile.config.Config; import org.eclipse.microprofile.config.ConfigProvider; -import io.quarkiverse.quinoa.QuinoaHandlerConfig; +import io.quarkiverse.quinoa.QuinoaDevProxyHandlerConfig; import io.quarkus.runtime.annotations.ConfigDocDefault; import io.quarkus.runtime.annotations.ConfigPhase; import io.quarkus.runtime.annotations.ConfigRoot; @@ -78,12 +78,6 @@ public interface QuinoaConfig { */ PackageManagerCommandConfig packageManagerCommand(); - /** - * Name of the index page. - */ - @WithDefault(DEFAULT_INDEX_PAGE) - String indexPage(); - /** * Indicate if the Web UI should also be tested during the build phase (i.e: npm test). * To be used in a {@link io.quarkus.test.junit.QuarkusTestProfile} to have Web UI test running during a @@ -120,7 +114,7 @@ public interface QuinoaConfig { boolean enableSPARouting(); /** - * List of path prefixes to be ignored by Quinoa. + * List of path prefixes to be ignored by Quinoa (SPA Handler and Dev-Proxy). */ @ConfigDocDefault("ignore values configured by 'quarkus.resteasy-reactive.path', 'quarkus.resteasy.path' and 'quarkus.http.non-application-root-path'") Optional> ignoredPathPrefixes(); @@ -135,28 +129,21 @@ static List getNormalizedIgnoredPathPrefixes(QuinoaConfig config) { Config allConfig = ConfigProvider.getConfig(); List defaultIgnore = new ArrayList<>(); readExternalConfigPath(allConfig, "quarkus.resteasy.path").ifPresent(defaultIgnore::add); + readExternalConfigPath(allConfig, "quarkus.rest.path").ifPresent(defaultIgnore::add); readExternalConfigPath(allConfig, "quarkus.resteasy-reactive.path").ifPresent(defaultIgnore::add); readExternalConfigPath(allConfig, "quarkus.http.non-application-root-path").ifPresent(defaultIgnore::add); return defaultIgnore; }).stream().map(s -> s.startsWith("/") ? s : "/" + s).collect(toList()); } - static QuinoaHandlerConfig toHandlerConfig(QuinoaConfig config, boolean devMode, + static QuinoaDevProxyHandlerConfig toDevProxyHandlerConfig(final QuinoaConfig config, final HttpBuildTimeConfig httpBuildTimeConfig) { final Set compressMediaTypes = httpBuildTimeConfig.compressMediaTypes.map(Set::copyOf).orElse(Set.of()); - final String indexPage = resolveIndexPage(config, devMode); - return new QuinoaHandlerConfig(getNormalizedIgnoredPathPrefixes(config), indexPage, devMode, + return new QuinoaDevProxyHandlerConfig(getNormalizedIgnoredPathPrefixes(config), + config.devServer().indexPage().orElse(DEFAULT_INDEX_PAGE), httpBuildTimeConfig.enableCompression, compressMediaTypes, config.devServer().directForwarding()); } - private static String resolveIndexPage(QuinoaConfig config, boolean devMode) { - if (!devMode) { - // Make sure we never return the devServer.indexPage() in non-dev mode - return config.indexPage(); - } - return isDevServerMode(config) ? config.devServer().indexPage().orElse(config.indexPage()) : config.indexPage(); - } - private static Optional readExternalConfigPath(Config config, String key) { return config.getOptionalValue(key, String.class) .filter(s -> !Objects.equals(s, "/")) @@ -193,9 +180,6 @@ static boolean isEqual(QuinoaConfig q1, QuinoaConfig q2) { if (!PackageManagerCommandConfig.isEqual(q1.packageManagerCommand(), q2.packageManagerCommand())) { return false; } - if (!Objects.equals(q1.indexPage(), q2.indexPage())) { - return false; - } if (!Objects.equals(q1.runTests(), q2.runTests())) { return false; } diff --git a/deployment/src/main/java/io/quarkiverse/quinoa/deployment/config/delegate/QuinoaConfigDelegate.java b/deployment/src/main/java/io/quarkiverse/quinoa/deployment/config/delegate/QuinoaConfigDelegate.java index f1cb76aa..b7b3dfbf 100644 --- a/deployment/src/main/java/io/quarkiverse/quinoa/deployment/config/delegate/QuinoaConfigDelegate.java +++ b/deployment/src/main/java/io/quarkiverse/quinoa/deployment/config/delegate/QuinoaConfigDelegate.java @@ -51,11 +51,6 @@ public PackageManagerCommandConfig packageManagerCommand() { return delegate.packageManagerCommand(); } - @Override - public String indexPage() { - return delegate.indexPage(); - } - @Override public boolean runTests() { return delegate.runTests(); diff --git a/deployment/src/main/java/io/quarkiverse/quinoa/deployment/items/BuiltResourcesBuildItem.java b/deployment/src/main/java/io/quarkiverse/quinoa/deployment/items/BuiltResourcesBuildItem.java index 53b4c9a0..648e1b8d 100644 --- a/deployment/src/main/java/io/quarkiverse/quinoa/deployment/items/BuiltResourcesBuildItem.java +++ b/deployment/src/main/java/io/quarkiverse/quinoa/deployment/items/BuiltResourcesBuildItem.java @@ -1,8 +1,6 @@ package io.quarkiverse.quinoa.deployment.items; import java.nio.file.Path; -import java.util.HashSet; -import java.util.Objects; import java.util.Optional; import java.util.Set; @@ -27,38 +25,10 @@ public Optional getDirectory() { return directory; } - public Set getNames() { - Set names = new HashSet<>(resources.size()); - for (BuiltResource entry : resources) { - names.add(entry.getName()); - } - return names; + public Set resources() { + return resources; } - public static class BuiltResource { - private final String name; - - public BuiltResource(String name) { - this.name = name; - } - - public String getName() { - return name; - } - - @Override - public boolean equals(Object o) { - if (this == o) - return true; - if (o == null || getClass() != o.getClass()) - return false; - BuiltResource entry = (BuiltResource) o; - return name.equals(entry.name); - } - - @Override - public int hashCode() { - return Objects.hash(name); - } + public record BuiltResource(String name, byte[] content) { } } diff --git a/deployment/src/test/java/io/quarkiverse/quinoa/test/QuinoaAbsoluteUIDirTest.java b/deployment/src/test/java/io/quarkiverse/quinoa/test/QuinoaAbsoluteUIDirTest.java index 2f880f70..9ad21056 100644 --- a/deployment/src/test/java/io/quarkiverse/quinoa/test/QuinoaAbsoluteUIDirTest.java +++ b/deployment/src/test/java/io/quarkiverse/quinoa/test/QuinoaAbsoluteUIDirTest.java @@ -24,8 +24,6 @@ public class QuinoaAbsoluteUIDirTest { assertThat(l) .anyMatch(s -> s.getMessage().equals("Running Quinoa package manager build command: %s") && s.getParameters()[0].equals(systemBinary("npm") + " run build")); - assertThat(l) - .anyMatch(s -> s.getMessage().equals("Quinoa is ignoring paths starting with: /q/")); }); static { diff --git a/deployment/src/test/java/io/quarkiverse/quinoa/test/QuinoaDefaultConfigTest.java b/deployment/src/test/java/io/quarkiverse/quinoa/test/QuinoaDefaultConfigTest.java index 8d95f8ac..89e2a61d 100644 --- a/deployment/src/test/java/io/quarkiverse/quinoa/test/QuinoaDefaultConfigTest.java +++ b/deployment/src/test/java/io/quarkiverse/quinoa/test/QuinoaDefaultConfigTest.java @@ -23,8 +23,6 @@ public class QuinoaDefaultConfigTest { assertThat(l) .anyMatch(s -> s.getMessage().equals("Running Quinoa package manager build command: %s") && s.getParameters()[0].equals(systemBinary("npm") + " run build")); - assertThat(l) - .anyMatch(s -> s.getMessage().equals("Quinoa is ignoring paths starting with: /q/")); }); @Test diff --git a/deployment/src/test/java/io/quarkiverse/quinoa/test/QuinoaIgnorePathPrefixesConfigTest.java b/deployment/src/test/java/io/quarkiverse/quinoa/test/QuinoaIgnorePathPrefixesConfigTest.java index ab13dca3..7cdac4c9 100644 --- a/deployment/src/test/java/io/quarkiverse/quinoa/test/QuinoaIgnorePathPrefixesConfigTest.java +++ b/deployment/src/test/java/io/quarkiverse/quinoa/test/QuinoaIgnorePathPrefixesConfigTest.java @@ -14,8 +14,10 @@ public class QuinoaIgnorePathPrefixesConfigTest { @RegisterExtension static final QuarkusUnitTest config = QuinoaQuarkusUnitTest.create(NAME).toQuarkusUnitTest() .overrideConfigKey("quarkus.quinoa.ignored-path-prefixes", "/foo/bar,/api,q,a/b") + .overrideConfigKey("quarkus.quinoa.enable-spa-routing", "true") .assertLogRecords(l -> assertThat(l) - .anyMatch(s -> s.getMessage().equals("Quinoa is ignoring paths starting with: /foo/bar, /api, /q, /a/b"))); + .anyMatch(s -> s.getMessage() + .equals("Quinoa SPA routing handler is ignoring paths starting with: /foo/bar, /api, /q, /a/b"))); @Test public void testQuinoa() { diff --git a/deployment/src/test/java/io/quarkiverse/quinoa/test/QuinoaPathPrefixesRESTConfigTest.java b/deployment/src/test/java/io/quarkiverse/quinoa/test/QuinoaPathPrefixesRESTConfigTest.java index 8b880888..77bee2b0 100644 --- a/deployment/src/test/java/io/quarkiverse/quinoa/test/QuinoaPathPrefixesRESTConfigTest.java +++ b/deployment/src/test/java/io/quarkiverse/quinoa/test/QuinoaPathPrefixesRESTConfigTest.java @@ -18,12 +18,13 @@ public class QuinoaPathPrefixesRESTConfigTest { @RegisterExtension static final QuarkusUnitTest config = QuinoaQuarkusUnitTest.create(NAME) .toQuarkusUnitTest() - .overrideConfigKey("quarkus.resteasy-reactive.path", "/foo/reactive") + .overrideConfigKey("quarkus.rest.path", "/foo/reactive") .overrideConfigKey("quarkus.resteasy.path", "/foo/classic") .overrideConfigKey("quarkus.http.non-application-root-path", "/bar/non") + .overrideConfigKey("quarkus.quinoa.enable-spa-routing", "true") .assertLogRecords(l -> assertThat(l) .anyMatch(s -> s.getMessage() - .equals("Quinoa is ignoring paths starting with: /foo/classic/, /foo/reactive/, /bar/non/"))); + .equals("Quinoa SPA routing handler is ignoring paths starting with: /foo/classic/, /foo/reactive/, /bar/non/"))); @Test public void testQuinoa() { diff --git a/docs/modules/ROOT/pages/includes/attributes.adoc b/docs/modules/ROOT/pages/includes/attributes.adoc index 843c5613..ef1d3d1c 100644 --- a/docs/modules/ROOT/pages/includes/attributes.adoc +++ b/docs/modules/ROOT/pages/includes/attributes.adoc @@ -1,4 +1,4 @@ -:quarkus-version: 3.8.2 +:quarkus-version: 3.12.0.CR1 :quarkus-quinoa-version: 2.3.8 :maven-version: 3.8.1+ :extension-status: stable diff --git a/docs/modules/ROOT/pages/includes/quarkus-quinoa.adoc b/docs/modules/ROOT/pages/includes/quarkus-quinoa.adoc index 07c0a910..d6c180ba 100644 --- a/docs/modules/ROOT/pages/includes/quarkus-quinoa.adoc +++ b/docs/modules/ROOT/pages/includes/quarkus-quinoa.adoc @@ -384,23 +384,6 @@ endif::add-copy-button-to-env-var[] |`framework detection with fallback to 'start'` -a|icon:lock[title=Fixed at build time] [[quarkus-quinoa_quarkus-quinoa-index-page]]`link:#quarkus-quinoa_quarkus-quinoa-index-page[quarkus.quinoa.index-page]` - - -[.description] --- -Name of the index page. - -ifdef::add-copy-button-to-env-var[] -Environment variable: env_var_with_copy_button:+++QUARKUS_QUINOA_INDEX_PAGE+++[] -endif::add-copy-button-to-env-var[] -ifndef::add-copy-button-to-env-var[] -Environment variable: `+++QUARKUS_QUINOA_INDEX_PAGE+++` -endif::add-copy-button-to-env-var[] ---|string -|`index.html` - - a|icon:lock[title=Fixed at build time] [[quarkus-quinoa_quarkus-quinoa-run-tests]]`link:#quarkus-quinoa_quarkus-quinoa-run-tests[quarkus.quinoa.run-tests]` @@ -491,7 +474,7 @@ a|icon:lock[title=Fixed at build time] [[quarkus-quinoa_quarkus-quinoa-ignored-p [.description] -- -List of path prefixes to be ignored by Quinoa. +List of path prefixes to be ignored by Quinoa (SPA Handler and Dev-Proxy). ifdef::add-copy-button-to-env-var[] Environment variable: env_var_with_copy_button:+++QUARKUS_QUINOA_IGNORED_PATH_PREFIXES+++[] @@ -673,7 +656,7 @@ endif::add-copy-button-to-env-var[] |`false` -a|icon:lock[title=Fixed at build time] [[quarkus-quinoa_quarkus-quinoa-package-manager-command-install-env-install-env]]`link:#quarkus-quinoa_quarkus-quinoa-package-manager-command-install-env-install-env[quarkus.quinoa.package-manager-command.install-env]` +a|icon:lock[title=Fixed at build time] [[quarkus-quinoa_quarkus-quinoa-package-manager-command-install-env-install-env]]`link:#quarkus-quinoa_quarkus-quinoa-package-manager-command-install-env-install-env[quarkus.quinoa.package-manager-command.install-env."install-env"]` [.description] @@ -681,16 +664,17 @@ a|icon:lock[title=Fixed at build time] [[quarkus-quinoa_quarkus-quinoa-package-m Environment variables for install command execution. ifdef::add-copy-button-to-env-var[] -Environment variable: env_var_with_copy_button:+++QUARKUS_QUINOA_PACKAGE_MANAGER_COMMAND_INSTALL_ENV+++[] +Environment variable: env_var_with_copy_button:+++QUARKUS_QUINOA_PACKAGE_MANAGER_COMMAND_INSTALL_ENV__INSTALL_ENV_+++[] endif::add-copy-button-to-env-var[] ifndef::add-copy-button-to-env-var[] -Environment variable: `+++QUARKUS_QUINOA_PACKAGE_MANAGER_COMMAND_INSTALL_ENV+++` +Environment variable: `+++QUARKUS_QUINOA_PACKAGE_MANAGER_COMMAND_INSTALL_ENV__INSTALL_ENV_+++` endif::add-copy-button-to-env-var[] ---|`Map` +--|link:https://docs.oracle.com/javase/8/docs/api/java/lang/String.html[String] + | -a|icon:lock[title=Fixed at build time] [[quarkus-quinoa_quarkus-quinoa-package-manager-command-ci-env-ci-env]]`link:#quarkus-quinoa_quarkus-quinoa-package-manager-command-ci-env-ci-env[quarkus.quinoa.package-manager-command.ci-env]` +a|icon:lock[title=Fixed at build time] [[quarkus-quinoa_quarkus-quinoa-package-manager-command-ci-env-ci-env]]`link:#quarkus-quinoa_quarkus-quinoa-package-manager-command-ci-env-ci-env[quarkus.quinoa.package-manager-command.ci-env."ci-env"]` [.description] @@ -698,16 +682,17 @@ a|icon:lock[title=Fixed at build time] [[quarkus-quinoa_quarkus-quinoa-package-m Environment variables for ci command execution. ifdef::add-copy-button-to-env-var[] -Environment variable: env_var_with_copy_button:+++QUARKUS_QUINOA_PACKAGE_MANAGER_COMMAND_CI_ENV+++[] +Environment variable: env_var_with_copy_button:+++QUARKUS_QUINOA_PACKAGE_MANAGER_COMMAND_CI_ENV__CI_ENV_+++[] endif::add-copy-button-to-env-var[] ifndef::add-copy-button-to-env-var[] -Environment variable: `+++QUARKUS_QUINOA_PACKAGE_MANAGER_COMMAND_CI_ENV+++` +Environment variable: `+++QUARKUS_QUINOA_PACKAGE_MANAGER_COMMAND_CI_ENV__CI_ENV_+++` endif::add-copy-button-to-env-var[] ---|`Map` +--|link:https://docs.oracle.com/javase/8/docs/api/java/lang/String.html[String] + | -a|icon:lock[title=Fixed at build time] [[quarkus-quinoa_quarkus-quinoa-package-manager-command-build-env-build-env]]`link:#quarkus-quinoa_quarkus-quinoa-package-manager-command-build-env-build-env[quarkus.quinoa.package-manager-command.build-env]` +a|icon:lock[title=Fixed at build time] [[quarkus-quinoa_quarkus-quinoa-package-manager-command-build-env-build-env]]`link:#quarkus-quinoa_quarkus-quinoa-package-manager-command-build-env-build-env[quarkus.quinoa.package-manager-command.build-env."build-env"]` [.description] @@ -715,16 +700,17 @@ a|icon:lock[title=Fixed at build time] [[quarkus-quinoa_quarkus-quinoa-package-m Environment variables for build command execution. ifdef::add-copy-button-to-env-var[] -Environment variable: env_var_with_copy_button:+++QUARKUS_QUINOA_PACKAGE_MANAGER_COMMAND_BUILD_ENV+++[] +Environment variable: env_var_with_copy_button:+++QUARKUS_QUINOA_PACKAGE_MANAGER_COMMAND_BUILD_ENV__BUILD_ENV_+++[] endif::add-copy-button-to-env-var[] ifndef::add-copy-button-to-env-var[] -Environment variable: `+++QUARKUS_QUINOA_PACKAGE_MANAGER_COMMAND_BUILD_ENV+++` +Environment variable: `+++QUARKUS_QUINOA_PACKAGE_MANAGER_COMMAND_BUILD_ENV__BUILD_ENV_+++` endif::add-copy-button-to-env-var[] ---|`Map` +--|link:https://docs.oracle.com/javase/8/docs/api/java/lang/String.html[String] + | -a|icon:lock[title=Fixed at build time] [[quarkus-quinoa_quarkus-quinoa-package-manager-command-test-env-test-env]]`link:#quarkus-quinoa_quarkus-quinoa-package-manager-command-test-env-test-env[quarkus.quinoa.package-manager-command.test-env]` +a|icon:lock[title=Fixed at build time] [[quarkus-quinoa_quarkus-quinoa-package-manager-command-test-env-test-env]]`link:#quarkus-quinoa_quarkus-quinoa-package-manager-command-test-env-test-env[quarkus.quinoa.package-manager-command.test-env."test-env"]` [.description] @@ -732,16 +718,17 @@ a|icon:lock[title=Fixed at build time] [[quarkus-quinoa_quarkus-quinoa-package-m Environment variables for test command execution. ifdef::add-copy-button-to-env-var[] -Environment variable: env_var_with_copy_button:+++QUARKUS_QUINOA_PACKAGE_MANAGER_COMMAND_TEST_ENV+++[] +Environment variable: env_var_with_copy_button:+++QUARKUS_QUINOA_PACKAGE_MANAGER_COMMAND_TEST_ENV__TEST_ENV_+++[] endif::add-copy-button-to-env-var[] ifndef::add-copy-button-to-env-var[] -Environment variable: `+++QUARKUS_QUINOA_PACKAGE_MANAGER_COMMAND_TEST_ENV+++` +Environment variable: `+++QUARKUS_QUINOA_PACKAGE_MANAGER_COMMAND_TEST_ENV__TEST_ENV_+++` endif::add-copy-button-to-env-var[] ---|`Map` +--|link:https://docs.oracle.com/javase/8/docs/api/java/lang/String.html[String] + | -a|icon:lock[title=Fixed at build time] [[quarkus-quinoa_quarkus-quinoa-package-manager-command-dev-env-dev-env]]`link:#quarkus-quinoa_quarkus-quinoa-package-manager-command-dev-env-dev-env[quarkus.quinoa.package-manager-command.dev-env]` +a|icon:lock[title=Fixed at build time] [[quarkus-quinoa_quarkus-quinoa-package-manager-command-dev-env-dev-env]]`link:#quarkus-quinoa_quarkus-quinoa-package-manager-command-dev-env-dev-env[quarkus.quinoa.package-manager-command.dev-env."dev-env"]` [.description] @@ -749,12 +736,13 @@ a|icon:lock[title=Fixed at build time] [[quarkus-quinoa_quarkus-quinoa-package-m Environment variables for development command execution. ifdef::add-copy-button-to-env-var[] -Environment variable: env_var_with_copy_button:+++QUARKUS_QUINOA_PACKAGE_MANAGER_COMMAND_DEV_ENV+++[] +Environment variable: env_var_with_copy_button:+++QUARKUS_QUINOA_PACKAGE_MANAGER_COMMAND_DEV_ENV__DEV_ENV_+++[] endif::add-copy-button-to-env-var[] ifndef::add-copy-button-to-env-var[] -Environment variable: `+++QUARKUS_QUINOA_PACKAGE_MANAGER_COMMAND_DEV_ENV+++` +Environment variable: `+++QUARKUS_QUINOA_PACKAGE_MANAGER_COMMAND_DEV_ENV__DEV_ENV_+++` endif::add-copy-button-to-env-var[] ---|`Map` +--|link:https://docs.oracle.com/javase/8/docs/api/java/lang/String.html[String] + | |=== \ No newline at end of file diff --git a/integration-tests/src/main/resources/application.properties b/integration-tests/src/main/resources/application.properties index e8d5f34e..326f1106 100644 --- a/integration-tests/src/main/resources/application.properties +++ b/integration-tests/src/main/resources/application.properties @@ -15,7 +15,7 @@ quarkus.quinoa.ui-dir=src/main/ui-react %angular.quarkus.quinoa.package-manager-install.yarn-version=1.22.19 %lit,lit-root-path.quarkus.quinoa.ui-dir=src/main/ui-lit %lit,lit-root-path.quarkus.quinoa.build-dir=dist -%lit,lit-root-path.quarkus.quinoa.index-page=app.html +%lit,lit-root-path.quarkus.http.static-resources.index-page=app.html %lit,lit-root-path.quarkus.quinoa.package-manager-command.build=run build-per-env %lit-root-path.quarkus.quinoa.package-manager-command.build-env.FOO=bar %lit.quarkus.quinoa.package-manager-command.build-env.FOO=bar diff --git a/pom.xml b/pom.xml index f0a867a2..c1072487 100644 --- a/pom.xml +++ b/pom.xml @@ -34,10 +34,10 @@ 3.13.0 - 11 + 17 UTF-8 UTF-8 - 3.8.2 + 3.12.0.CR1 1.0.0 3.26.0 1.15.0 diff --git a/runtime/src/main/java/io/quarkiverse/quinoa/QuinoaDevProxyHandler.java b/runtime/src/main/java/io/quarkiverse/quinoa/QuinoaDevProxyHandler.java index 268be9bd..973a625f 100644 --- a/runtime/src/main/java/io/quarkiverse/quinoa/QuinoaDevProxyHandler.java +++ b/runtime/src/main/java/io/quarkiverse/quinoa/QuinoaDevProxyHandler.java @@ -34,9 +34,9 @@ class QuinoaDevProxyHandler implements Handler { private final WebClient client; private final QuinoaDevWebSocketProxyHandler wsUpgradeHandler; private final ClassLoader currentClassLoader; - private final QuinoaHandlerConfig config; + private final QuinoaDevProxyHandlerConfig config; - QuinoaDevProxyHandler(final QuinoaHandlerConfig config, final Vertx vertx, String host, int port, + QuinoaDevProxyHandler(final QuinoaDevProxyHandlerConfig config, final Vertx vertx, String host, int port, boolean websocket) { this.host = host; this.port = port; diff --git a/runtime/src/main/java/io/quarkiverse/quinoa/QuinoaHandlerConfig.java b/runtime/src/main/java/io/quarkiverse/quinoa/QuinoaDevProxyHandlerConfig.java similarity index 74% rename from runtime/src/main/java/io/quarkiverse/quinoa/QuinoaHandlerConfig.java rename to runtime/src/main/java/io/quarkiverse/quinoa/QuinoaDevProxyHandlerConfig.java index 26d6447c..e3a61cd2 100644 --- a/runtime/src/main/java/io/quarkiverse/quinoa/QuinoaHandlerConfig.java +++ b/runtime/src/main/java/io/quarkiverse/quinoa/QuinoaDevProxyHandlerConfig.java @@ -6,21 +6,19 @@ import io.quarkus.runtime.annotations.RecordableConstructor; -public class QuinoaHandlerConfig { +public class QuinoaDevProxyHandlerConfig { public final List ignoredPathPrefixes; public final String indexPage; - public final boolean devMode; public final boolean enableCompression; public final Set compressMediaTypes; public final boolean devServerDirectForwarding; @RecordableConstructor - public QuinoaHandlerConfig(List ignoredPathPrefixes, String indexPage, boolean devMode, boolean enableCompression, + public QuinoaDevProxyHandlerConfig(List ignoredPathPrefixes, String indexPage, boolean enableCompression, Set compressMediaTypes, boolean devServerDirectForwarding) { this.ignoredPathPrefixes = ignoredPathPrefixes; this.indexPage = "/".equals(indexPage) ? "" : indexPage; - this.devMode = devMode; this.enableCompression = enableCompression; this.compressMediaTypes = compressMediaTypes; this.devServerDirectForwarding = devServerDirectForwarding; @@ -32,8 +30,8 @@ public boolean equals(Object o) { return true; if (o == null || getClass() != o.getClass()) return false; - QuinoaHandlerConfig that = (QuinoaHandlerConfig) o; - return devMode == that.devMode && enableCompression == that.enableCompression + QuinoaDevProxyHandlerConfig that = (QuinoaDevProxyHandlerConfig) o; + return enableCompression == that.enableCompression && devServerDirectForwarding == that.devServerDirectForwarding && Objects.equals(ignoredPathPrefixes, that.ignoredPathPrefixes) && Objects.equals(indexPage, that.indexPage) && Objects.equals(compressMediaTypes, that.compressMediaTypes); @@ -41,7 +39,7 @@ public boolean equals(Object o) { @Override public int hashCode() { - return Objects.hash(ignoredPathPrefixes, indexPage, devMode, enableCompression, compressMediaTypes, + return Objects.hash(ignoredPathPrefixes, indexPage, enableCompression, compressMediaTypes, devServerDirectForwarding); } } diff --git a/runtime/src/main/java/io/quarkiverse/quinoa/QuinoaRecorder.java b/runtime/src/main/java/io/quarkiverse/quinoa/QuinoaRecorder.java index c57ffa32..d3401e53 100644 --- a/runtime/src/main/java/io/quarkiverse/quinoa/QuinoaRecorder.java +++ b/runtime/src/main/java/io/quarkiverse/quinoa/QuinoaRecorder.java @@ -25,20 +25,20 @@ public class QuinoaRecorder { public static final int QUINOA_SPA_ROUTE_ORDER = ROUTE_ORDER_DEFAULT + 30_000; public static final Set HANDLED_METHODS = Set.of(HttpMethod.HEAD, HttpMethod.OPTIONS, HttpMethod.GET); - public Handler quinoaProxyDevHandler(final QuinoaHandlerConfig handlerConfig, Supplier vertx, + public Handler quinoaProxyDevHandler(final QuinoaDevProxyHandlerConfig handlerConfig, Supplier vertx, String host, int port, boolean websocket) { - logIgnoredPathPrefixes(handlerConfig.ignoredPathPrefixes); + if (LOG.isDebugEnabled()) { + LOG.debugf("Quinoa dev proxy-handler is ignoring paths starting with: " + + String.join(", ", handlerConfig.ignoredPathPrefixes)); + } return new QuinoaDevProxyHandler(handlerConfig, vertx.get(), host, port, websocket); } - public Handler quinoaSPARoutingHandler(final QuinoaHandlerConfig handlerConfig) throws IOException { - return new QuinoaSPARoutingHandler(handlerConfig); - } - - public Handler quinoaHandler(final QuinoaHandlerConfig handlerConfig, final String directory, - final Set uiResources) { - logIgnoredPathPrefixes(handlerConfig.ignoredPathPrefixes); - return new QuinoaUIResourceHandler(handlerConfig, directory, uiResources); + public Handler quinoaSPARoutingHandler(List ignoredPathPrefixes) throws IOException { + if (LOG.isDebugEnabled()) { + LOG.debugf("Quinoa SPA routing handler is ignoring paths starting with: " + String.join(", ", ignoredPathPrefixes)); + } + return new QuinoaSPARoutingHandler(ignoredPathPrefixes); } static String resolvePath(RoutingContext ctx) { @@ -57,7 +57,7 @@ static boolean isIgnored(final String path, final List ignoredPathPrefix return false; } - static void compressIfNeeded(QuinoaHandlerConfig config, RoutingContext ctx, String path) { + static void compressIfNeeded(QuinoaDevProxyHandlerConfig config, RoutingContext ctx, String path) { if (config.enableCompression && isCompressed(config, path)) { // VertxHttpRecorder is adding "Content-Encoding: identity" to all requests if // compression is enabled. @@ -67,7 +67,7 @@ static void compressIfNeeded(QuinoaHandlerConfig config, RoutingContext ctx, Str } } - private static boolean isCompressed(QuinoaHandlerConfig config, String path) { + private static boolean isCompressed(QuinoaDevProxyHandlerConfig config, String path) { if (config.compressMediaTypes.isEmpty()) { return false; } @@ -75,12 +75,6 @@ private static boolean isCompressed(QuinoaHandlerConfig config, String path) { return contentType != null && config.compressMediaTypes.contains(contentType); } - static void logIgnoredPathPrefixes(final List ignoredPathPrefixes) { - if (LOG.isDebugEnabled()) { - LOG.debugf("Quinoa is ignoring paths starting with: " + String.join(", ", ignoredPathPrefixes)); - } - } - static boolean shouldHandleMethod(RoutingContext ctx) { return HANDLED_METHODS.contains(ctx.request().method()); } diff --git a/runtime/src/main/java/io/quarkiverse/quinoa/QuinoaSPARoutingHandler.java b/runtime/src/main/java/io/quarkiverse/quinoa/QuinoaSPARoutingHandler.java index 8cbbaa5a..ec42a562 100644 --- a/runtime/src/main/java/io/quarkiverse/quinoa/QuinoaSPARoutingHandler.java +++ b/runtime/src/main/java/io/quarkiverse/quinoa/QuinoaSPARoutingHandler.java @@ -5,6 +5,7 @@ import static io.quarkiverse.quinoa.QuinoaRecorder.resolvePath; import static io.quarkiverse.quinoa.QuinoaRecorder.shouldHandleMethod; +import java.util.List; import java.util.Objects; import org.jboss.logging.Logger; @@ -15,10 +16,10 @@ class QuinoaSPARoutingHandler implements Handler { private static final Logger LOG = Logger.getLogger(QuinoaSPARoutingHandler.class); private final ClassLoader currentClassLoader; - private final QuinoaHandlerConfig config; + private final List ignoredPathPrefixes; - public QuinoaSPARoutingHandler(final QuinoaHandlerConfig config) { - this.config = config; + public QuinoaSPARoutingHandler(List ignoredPathPrefixes) { + this.ignoredPathPrefixes = ignoredPathPrefixes; currentClassLoader = Thread.currentThread().getContextClassLoader(); } @@ -29,7 +30,7 @@ public void handle(RoutingContext ctx) { return; } String path = resolvePath(ctx); - if (!Objects.equals(path, "/") && !isIgnored(path, config.ignoredPathPrefixes)) { + if (!Objects.equals(path, "/") && !isIgnored(path, ignoredPathPrefixes)) { LOG.debugf("Quinoa is re-routing SPA request '%s' to '/'", ctx.normalizedPath()); ctx.reroute(ctx.mountPoint() != null ? ctx.mountPoint() : "/"); } else { diff --git a/runtime/src/main/java/io/quarkiverse/quinoa/QuinoaUIResourceHandler.java b/runtime/src/main/java/io/quarkiverse/quinoa/QuinoaUIResourceHandler.java deleted file mode 100644 index 0c2842e3..00000000 --- a/runtime/src/main/java/io/quarkiverse/quinoa/QuinoaUIResourceHandler.java +++ /dev/null @@ -1,103 +0,0 @@ -package io.quarkiverse.quinoa; - -import static io.quarkiverse.quinoa.QuinoaRecorder.compressIfNeeded; -import static io.quarkiverse.quinoa.QuinoaRecorder.isIgnored; -import static io.quarkiverse.quinoa.QuinoaRecorder.next; -import static io.quarkiverse.quinoa.QuinoaRecorder.resolvePath; -import static io.quarkiverse.quinoa.QuinoaRecorder.shouldHandleMethod; - -import java.net.URLEncoder; -import java.nio.charset.StandardCharsets; -import java.util.HashSet; -import java.util.Set; - -import org.jboss.logging.Logger; - -import io.quarkus.runtime.util.StringUtil; -import io.vertx.core.Handler; -import io.vertx.ext.web.RoutingContext; -import io.vertx.ext.web.handler.FileSystemAccess; -import io.vertx.ext.web.handler.StaticHandler; - -class QuinoaUIResourceHandler implements Handler { - private static final Logger LOG = Logger.getLogger(QuinoaUIResourceHandler.class); - - private final QuinoaHandlerConfig config; - private final Set uiResources; - private final Handler handler; - private final ClassLoader currentClassLoader; - - QuinoaUIResourceHandler(final QuinoaHandlerConfig config, final String directory, final Set uiResources) { - this.config = config; - this.uiResources = new HashSet<>(uiResources.size()); - for (String uiResource : uiResources) { - final String encoded = encodeURI(uiResource); - this.uiResources.add(encoded); - LOG.debugf("Quinoa UI encoded: '%s'", encoded); - } - handler = createStaticHandler(config, directory); - currentClassLoader = Thread.currentThread().getContextClassLoader(); - } - - @Override - public void handle(RoutingContext ctx) { - if (!shouldHandleMethod(ctx)) { - next(currentClassLoader, ctx); - return; - } - final String path = resolvePath(ctx); - if (isIgnored(path, config.ignoredPathPrefixes)) { - next(currentClassLoader, ctx); - return; - } - final String resourcePath = path.endsWith("/") ? path + config.indexPage : path; - LOG.debugf("Quinoa is checking: '%s'", resourcePath); - if (!isIgnored(resourcePath, config.ignoredPathPrefixes) && uiResources.contains(resourcePath)) { - LOG.debugf("Quinoa is serving: '%s'", resourcePath); - compressIfNeeded(config, ctx, resourcePath); - handler.handle(ctx); - } else { - next(currentClassLoader, ctx); - } - } - - private static Handler createStaticHandler(QuinoaHandlerConfig config, String directory) { - LOG.debugf("Static Index: '%s'", config.indexPage); - if (StringUtil.isNullOrEmpty(config.indexPage)) { - throw new IllegalStateException("Static index page is not configured!"); - } - final StaticHandler staticHandler = directory != null ? StaticHandler.create(FileSystemAccess.ROOT, directory) - : StaticHandler.create(QuinoaRecorder.META_INF_WEB_UI); - staticHandler.setDefaultContentEncoding(StandardCharsets.UTF_8.name()); - staticHandler.setIndexPage(config.indexPage); - staticHandler.setCachingEnabled(!config.devMode); - return staticHandler; - } - - /** - * Duplicate code from OmniFaces project under apache license: - * https://github.com/omnifaces/omnifaces/blob/develop/license.txt - *

- * URI-encode the given string using UTF-8. URIs (paths and filenames) have different encoding rules as compared to - * URL query string parameters. {@link URLEncoder} is actually only for www (HTML) form based query string parameter - * values (as used when a webbrowser submits a HTML form). URI encoding has a lot in common with URL encoding, but - * the space has to be %20 and some chars doesn't necessarily need to be encoded. - * - * @param string The string to be URI-encoded using UTF-8. - * @return The given string, URI-encoded using UTF-8, or null if null was given. - */ - private static String encodeURI(String string) { - if (string == null) { - return null; - } - - return URLEncoder.encode(string, StandardCharsets.UTF_8) - .replace("+", "%20") - .replace("%21", "!") - .replace("%27", "'") - .replace("%28", "(") - .replace("%29", ")") - .replace("%2F", "/") - .replace("%7E", "~"); - } -}