This is an automated email from the ASF dual-hosted git repository. thiagohp pushed a commit to branch javax in repository https://gitbox.apache.org/repos/asf/tapestry-5.git
The following commit(s) were added to refs/heads/javax by this push: new 209680b59 TAP5-2803: foundation work for ES module support 209680b59 is described below commit 209680b59a1f361abd8d46e9c28da355af7daccc Author: Thiago H. de Paula Figueiredo <thi...@arsmachina.com.br> AuthorDate: Tue Apr 1 15:31:17 2025 -0300 TAP5-2803: foundation work for ES module support TAP5-2803: fixing JavaDoc errors TAP5-2803: adding @Import(esModule) TAP5-2803: fixing DocumentLinkerImplTest failures TAP5-2803: fixing live reloading of files in /META-INF/es-assets TAP5-2803: fixing broken commit_with_no_javascript test TAP5-2803: Implemented EsModuleInitialization.invoke() and .with(). TAP5-2803: supporting Number and Boolean in ES module's .with() plus tests to verify all values were passed correctly --- 5_10_RELEASE_NOTES.md | 24 ++ .../java/org/apache/tapestry5/SymbolConstants.java | 14 +- .../org/apache/tapestry5/annotations/Import.java | 10 + .../internal/services/DocumentLinker.java | 16 + .../internal/services/DocumentLinkerImpl.java | 41 ++- .../internal/services/EsModuleInitsManager.java | 61 ++++ .../services/PartialMarkupDocumentLinker.java | 17 +- .../internal/services/ajax/BaseInitialization.java | 27 ++ .../services/ajax/EsModuleInitializationImpl.java | 67 ++++ .../internal/services/ajax/InitializationImpl.java | 37 +++ .../services/ajax/JavaScriptSupportImpl.java | 93 +++--- .../services/assets/ResourceChangeTrackerImpl.java | 3 +- .../services/javascript/EsModuleManagerImpl.java | 345 +++++++++++++++++++++ .../tapestry5/internal/transform/ImportWorker.java | 48 ++- .../apache/tapestry5/modules/JavaScriptModule.java | 4 + .../apache/tapestry5/modules/TapestryModule.java | 16 +- ...ialization.java => AbstractInitialization.java} | 20 +- .../javascript/EsModuleConfigurationCallback.java | 62 ++++ .../javascript/EsModuleInitialization.java | 96 ++++++ .../services/javascript/EsModuleManager.java | 57 ++++ .../services/javascript/ImportPlacement.java | 37 +++ .../services/javascript/Initialization.java | 2 +- .../services/javascript/JavaScriptSupport.java | 19 ++ .../javascript/ModuleConfigurationCallback.java | 2 +- .../services/DocumentLinkerImplTest.groovy | 30 +- .../tapestry5/integration/app1/EsModuleTests.java | 221 +++++++++++++ .../integration/app1/pages/EsModuleDemo.java | 107 +++++++ .../tapestry5/integration/app1/pages/Index.java | 4 +- .../integration/app1/services/AppModule.java | 40 +++ .../javascript/EsModuleManagerImplTest.java | 93 ++++++ .../META-INF/assets/es-modules/default-export.js | 3 + .../META-INF/assets/es-modules/foo/bar.js | 2 + .../assets/es-modules/non-default-export.js | 6 + .../es-modules/parameter-type-default-export.js | 14 + .../es-modules/parameterless-default-export.js | 3 + .../assets/es-modules/placement/body-bottom.js | 2 + .../assets/es-modules/placement/body-top.js | 2 + .../META-INF/assets/es-modules/placement/head.js | 2 + .../META-INF/assets/es-modules/root-folder.js | 2 + .../META-INF/assets/es-modules/show-import-map.js | 2 + .../integration/app1/es-module-outside-metainf.js | 2 + .../integration/app1/pages/EsModuleDemo.tml | 34 ++ .../ioc/internal/util/URLChangeTracker.java | 21 ++ 43 files changed, 1605 insertions(+), 103 deletions(-) diff --git a/5_10_RELEASE_NOTES.md b/5_10_RELEASE_NOTES.md new file mode 100644 index 000000000..e7d1c36da --- /dev/null +++ b/5_10_RELEASE_NOTES.md @@ -0,0 +1,24 @@ +Scratch pad for changes destined for the 5.10.0 release notes page. + +# Added configuration symbols + +* `tapestry.es-module-path-prefix` (`SymbolConstants.ES_MODULE_PATH_PREFIX`) + + +# Added methods + +* `JavaScriptSupport.importEsModule(String moduleName)` +* `JavaScriptSupport.addEsModuleConfigurationCallback(EsModuleConfigurationCallback callback)` +* `org.apache.tapestry5.annotations.Import.esModule()` + +# Added types + +* `org.apache.tapestry5.services.javascript.EsModuleInitialization` +* `org.apache.tapestry5.services.javascript.ImportPlacement` +* `org.apache.tapestry5.services.javascript.EsModuleConfigurationCallback` +* `org.apache.tapestry5.services.javascript.EsModuleManager` + +# Non-backward-compatible changes (but that probably won't cause problems) + + +# Overall notes diff --git a/tapestry-core/src/main/java/org/apache/tapestry5/SymbolConstants.java b/tapestry-core/src/main/java/org/apache/tapestry5/SymbolConstants.java index dc19b2756..51a304c2c 100644 --- a/tapestry-core/src/main/java/org/apache/tapestry5/SymbolConstants.java +++ b/tapestry-core/src/main/java/org/apache/tapestry5/SymbolConstants.java @@ -376,7 +376,7 @@ public class SymbolConstants /** - * Prefix used for all module resources. This may contain slashes, but should not being or end with one. + * Prefix used for all Require.js module resources. This may contain slashes, but should not being or end with one. * Tapestry will create two {@link org.apache.tapestry5.http.services.Dispatcher}s from this: one for normal * modules, the other for GZip compressed modules (by appending ".gz" to this value). * @@ -385,6 +385,18 @@ public class SymbolConstants * @since 5.4 */ public static final String MODULE_PATH_PREFIX = "tapestry.module-path-prefix"; + + /** + * Prefix used for automatically configured ES module resources. + * This may contain slashes, but should not being or end with one. + * + * The default is "es-modules". + * + * TODO remove + * + * @since 5.4 + */ + public static final String ES_MODULE_PATH_PREFIX = "tapestry.es-module-path-prefix"; /** * Identifies the context path of the application, as determined from {@link javax.servlet.ServletContext#getContextPath()}. diff --git a/tapestry-core/src/main/java/org/apache/tapestry5/annotations/Import.java b/tapestry-core/src/main/java/org/apache/tapestry5/annotations/Import.java index 2e50e3f7c..9eec6ac5d 100644 --- a/tapestry-core/src/main/java/org/apache/tapestry5/annotations/Import.java +++ b/tapestry-core/src/main/java/org/apache/tapestry5/annotations/Import.java @@ -82,4 +82,14 @@ public @interface Import * @since 5.4 */ String[] module() default {}; + + /** + * Ids of ES modules to import. + * + * @see org.apache.tapestry5.services.javascript.EsModuleManager + * @see JavaScriptSupport#importEsModule(String) + * @since 5.10.0 + */ + String[] esModule() default {}; + } diff --git a/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/DocumentLinker.java b/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/DocumentLinker.java index e63b014ab..9326f12f0 100644 --- a/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/DocumentLinker.java +++ b/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/DocumentLinker.java @@ -13,6 +13,8 @@ package org.apache.tapestry5.internal.services; import org.apache.tapestry5.json.JSONArray; +import org.apache.tapestry5.services.javascript.EsModuleConfigurationCallback; +import org.apache.tapestry5.services.javascript.EsModuleInitialization; import org.apache.tapestry5.services.javascript.InitializationPriority; import org.apache.tapestry5.services.javascript.ModuleConfigurationCallback; import org.apache.tapestry5.services.javascript.StylesheetLink; @@ -55,6 +57,14 @@ public interface DocumentLinker * @since 5.4 */ void addModuleConfigurationCallback(ModuleConfigurationCallback callback); + + /** + * Adds an ES module configuration callback for this request. + * + * @param callback a {@link EsModuleConfigurationCallback}. It cannot be null. + * @since 5.10.0 + */ + void addEsModuleConfigurationCallback(EsModuleConfigurationCallback callback); /** * Adds JavaScript code. The code is collected into a single block that is injected just before the close body tag @@ -88,4 +98,10 @@ public interface DocumentLinker String moduleName, String functionName, JSONArray arguments); + + /** + * Adds ES module initialization. + * @since 5.10.0 + */ + void addEsModuleInitialization(EsModuleInitialization initialization); } diff --git a/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/DocumentLinkerImpl.java b/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/DocumentLinkerImpl.java index fc0449818..8814e7faf 100644 --- a/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/DocumentLinkerImpl.java +++ b/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/DocumentLinkerImpl.java @@ -16,6 +16,9 @@ import org.apache.tapestry5.commons.util.CollectionFactory; import org.apache.tapestry5.dom.Document; import org.apache.tapestry5.dom.Element; import org.apache.tapestry5.json.JSONArray; +import org.apache.tapestry5.services.javascript.EsModuleConfigurationCallback; +import org.apache.tapestry5.services.javascript.EsModuleInitialization; +import org.apache.tapestry5.services.javascript.EsModuleManager; import org.apache.tapestry5.services.javascript.InitializationPriority; import org.apache.tapestry5.services.javascript.ModuleConfigurationCallback; import org.apache.tapestry5.services.javascript.ModuleManager; @@ -34,12 +37,18 @@ public class DocumentLinkerImpl implements DocumentLinker private final List<String> libraryURLs = CollectionFactory.newList(); private final ModuleInitsManager initsManager = new ModuleInitsManager(); + + private final EsModuleInitsManager esModulesinitsManager = new EsModuleInitsManager(); private final List<ModuleConfigurationCallback> moduleConfigurationCallbacks = CollectionFactory.newList(); - + + private final List<EsModuleConfigurationCallback> esModuleConfigurationCallbacks = CollectionFactory.newList(); + private final List<StylesheetLink> includedStylesheets = CollectionFactory.newList(); private final ModuleManager moduleManager; + + private final EsModuleManager esModuleManager; private final boolean omitGeneratorMetaTag, enablePageloadingMask; @@ -56,9 +65,11 @@ public class DocumentLinkerImpl implements DocumentLinker * @param enablePageloadingMask * @param tapestryVersion */ - public DocumentLinkerImpl(ModuleManager moduleManager, boolean omitGeneratorMetaTag, boolean enablePageloadingMask, String tapestryVersion) + public DocumentLinkerImpl(ModuleManager moduleManager, EsModuleManager esModuleManager, + boolean omitGeneratorMetaTag, boolean enablePageloadingMask, String tapestryVersion) { this.moduleManager = moduleManager; + this.esModuleManager = esModuleManager; this.omitGeneratorMetaTag = omitGeneratorMetaTag; this.enablePageloadingMask = enablePageloadingMask; @@ -85,6 +96,7 @@ public class DocumentLinkerImpl implements DocumentLinker hasScriptsOrInitializations = true; } + @SuppressWarnings("deprecation") public void addScript(InitializationPriority priority, String script) { addInitialization(priority, "t5/core/pageinit", "evalJavaScript", new JSONArray().put(script)); @@ -114,6 +126,7 @@ public class DocumentLinkerImpl implements DocumentLinker return; } + // TAP5-2200: Generating XML from pages and templates is not possible anymore // only add JavaScript and CSS if we're actually generating final String mimeType = document.getMimeType(); @@ -121,7 +134,7 @@ public class DocumentLinkerImpl implements DocumentLinker { return; } - + addStylesheetsToHead(root, includedStylesheets); // only add the generator meta only to html documents @@ -138,6 +151,14 @@ public class DocumentLinkerImpl implements DocumentLinker } addScriptElements(root); + + final List<EsModuleInitialization> esModuleInits = esModulesinitsManager.getInits(); + if (isHtmlRoot && !esModuleInits.isEmpty()) + { + esModuleManager.writeImportMap(root.find("head"), esModuleConfigurationCallbacks); + esModuleManager.writeImports(root, esModuleInits); + } + } private static Element addElementBefore(Element container, Element insertionPoint, String name, String... namesAndValues) @@ -305,5 +326,19 @@ public class DocumentLinkerImpl implements DocumentLinker assert callback != null; moduleConfigurationCallbacks.add(callback); } + + public void addEsModuleConfigurationCallback(EsModuleConfigurationCallback callback) + { + assert callback != null; + esModuleConfigurationCallbacks.add(callback); + } + @Override + public void addEsModuleInitialization(EsModuleInitialization initialization) + { + assert initialization != null; + esModulesinitsManager.add(initialization); + hasScriptsOrInitializations = true; + } + } diff --git a/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/EsModuleInitsManager.java b/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/EsModuleInitsManager.java new file mode 100644 index 000000000..e3fc767fe --- /dev/null +++ b/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/EsModuleInitsManager.java @@ -0,0 +1,61 @@ +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package org.apache.tapestry5.internal.services; + +import java.util.List; +import java.util.Set; + +import org.apache.tapestry5.commons.util.CollectionFactory; +import org.apache.tapestry5.internal.services.ajax.EsModuleInitializationImpl; +import org.apache.tapestry5.services.javascript.EsModuleInitialization; + +public class EsModuleInitsManager +{ + private final Set<String> modules = CollectionFactory.newSet(); + + private final List<EsModuleInitialization> initializations = CollectionFactory.newList(); + + public void add(EsModuleInitialization initialization) + { + assert initialization != null; + + // We ignore a module being added again. + final String moduleName = ((EsModuleInitializationImpl) initialization).getModuleId(); + if (!modules.contains(moduleName)) + { + initializations.add(initialization); + modules.add(moduleName); + } + } + + /** + * Returns all previously added inits. + */ + public List<EsModuleInitialization> getInits() + { + return initializations; + } +} diff --git a/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/PartialMarkupDocumentLinker.java b/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/PartialMarkupDocumentLinker.java index 9cde60c17..9ccf81629 100644 --- a/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/PartialMarkupDocumentLinker.java +++ b/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/PartialMarkupDocumentLinker.java @@ -17,6 +17,9 @@ package org.apache.tapestry5.internal.services; import org.apache.tapestry5.internal.InternalConstants; import org.apache.tapestry5.json.JSONArray; import org.apache.tapestry5.json.JSONObject; +import org.apache.tapestry5.services.javascript.EsModuleConfigurationCallback; +import org.apache.tapestry5.services.javascript.EsModuleInitialization; +import org.apache.tapestry5.services.javascript.EsModuleManager; import org.apache.tapestry5.services.javascript.InitializationPriority; import org.apache.tapestry5.services.javascript.ModuleConfigurationCallback; import org.apache.tapestry5.services.javascript.StylesheetLink; @@ -30,7 +33,7 @@ public class PartialMarkupDocumentLinker implements DocumentLinker private final JSONArray stylesheets = new JSONArray(); private final ModuleInitsManager initsManager = new ModuleInitsManager(); - + public void addCoreLibrary(String libraryURL) { notImplemented("addCoreLibrary"); @@ -72,6 +75,18 @@ public class PartialMarkupDocumentLinker implements DocumentLinker initsManager.addInitialization(priority, moduleName, functionName, arguments); } + @Override + public void addEsModuleConfigurationCallback(EsModuleConfigurationCallback callback) + { + notImplemented("moduleConfigurationCallback"); + } + + @Override + public void addEsModuleInitialization(EsModuleInitialization initialization) + { + notImplemented("addEsModuleInitialization"); + } + /** * Commits changes, adding one or more keys to the reply. * diff --git a/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/ajax/BaseInitialization.java b/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/ajax/BaseInitialization.java new file mode 100644 index 000000000..589a8850a --- /dev/null +++ b/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/ajax/BaseInitialization.java @@ -0,0 +1,27 @@ +package org.apache.tapestry5.internal.services.ajax; + +import org.apache.tapestry5.ioc.internal.util.InternalUtils; +import org.apache.tapestry5.services.javascript.AbstractInitialization; + +abstract class BaseInitialization<T extends AbstractInitialization<?>> implements AbstractInitialization<T> +{ + final String moduleName; + + protected String functionName; + + BaseInitialization(String moduleName) + { + this.moduleName = moduleName; + } + + @SuppressWarnings("unchecked") + public T invoke(String functionName) + { + assert InternalUtils.isNonBlank(functionName); + + this.functionName = functionName; + + return (T) this; + } + +} \ No newline at end of file diff --git a/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/ajax/EsModuleInitializationImpl.java b/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/ajax/EsModuleInitializationImpl.java new file mode 100644 index 000000000..3b3bfeb4a --- /dev/null +++ b/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/ajax/EsModuleInitializationImpl.java @@ -0,0 +1,67 @@ +package org.apache.tapestry5.internal.services.ajax; + +import java.util.Collections; +import java.util.Map; + +import org.apache.tapestry5.commons.util.CollectionFactory; +import org.apache.tapestry5.services.javascript.EsModuleInitialization; +import org.apache.tapestry5.services.javascript.ImportPlacement; + +public class EsModuleInitializationImpl extends BaseInitialization<EsModuleInitialization> implements EsModuleInitialization +{ + + private Map<String, String> attributes; + private ImportPlacement placement = ImportPlacement.BODY_BOTTOM; + private Object[] arguments; + + EsModuleInitializationImpl(String moduleName) + { + super(moduleName); + } + + public EsModuleInitialization withAttribute(String id, String value) + { + if (attributes == null) + { + attributes = CollectionFactory.newMap(); + } + attributes.put(id, value); + return this; + } + + public EsModuleInitialization placement(ImportPlacement placement) + { + this.placement = placement; + return null; + } + + public String getModuleId() { + return moduleName; + } + + public Map<String, String> getAttributes() { + return attributes != null ? + Collections.unmodifiableMap(attributes) : + Collections.emptyMap(); + } + + public ImportPlacement getPlacement() { + return placement; + } + + public String getFunctionName() { + return functionName; + } + + @Override + public void with(Object... arguments) + { + this.arguments = arguments; + } + + public Object[] getArguments() + { + return arguments; + } + +} \ No newline at end of file diff --git a/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/ajax/InitializationImpl.java b/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/ajax/InitializationImpl.java new file mode 100644 index 000000000..59e2ff974 --- /dev/null +++ b/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/ajax/InitializationImpl.java @@ -0,0 +1,37 @@ +package org.apache.tapestry5.internal.services.ajax; + +import org.apache.tapestry5.json.JSONArray; +import org.apache.tapestry5.services.javascript.Initialization; +import org.apache.tapestry5.services.javascript.InitializationPriority; + +class InitializationImpl extends BaseInitialization<Initialization> implements Initialization +{ + + JSONArray arguments; + + InitializationPriority priority = InitializationPriority.NORMAL; + + public InitializationImpl(String moduleName) + { + super(moduleName); + } + + public Initialization priority(InitializationPriority priority) + { + assert priority != null; + + this.priority = priority; + + return this; + } + + @Override + public void with(Object... arguments) + { + assert arguments != null; + + this.arguments = new JSONArray(arguments); + } + + +} \ No newline at end of file diff --git a/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/ajax/JavaScriptSupportImpl.java b/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/ajax/JavaScriptSupportImpl.java index 3a98ac74d..5cf4dbd3b 100644 --- a/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/ajax/JavaScriptSupportImpl.java +++ b/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/ajax/JavaScriptSupportImpl.java @@ -12,6 +12,12 @@ package org.apache.tapestry5.internal.services.ajax; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.Map; +import java.util.Set; + import org.apache.tapestry5.Asset; import org.apache.tapestry5.BooleanHook; import org.apache.tapestry5.ComponentResources; @@ -26,9 +32,15 @@ import org.apache.tapestry5.ioc.internal.util.InternalUtils; import org.apache.tapestry5.ioc.util.IdAllocator; import org.apache.tapestry5.json.JSONArray; import org.apache.tapestry5.json.JSONObject; -import org.apache.tapestry5.services.javascript.*; - -import java.util.*; +import org.apache.tapestry5.services.javascript.EsModuleConfigurationCallback; +import org.apache.tapestry5.services.javascript.EsModuleInitialization; +import org.apache.tapestry5.services.javascript.Initialization; +import org.apache.tapestry5.services.javascript.InitializationPriority; +import org.apache.tapestry5.services.javascript.JavaScriptStack; +import org.apache.tapestry5.services.javascript.JavaScriptStackSource; +import org.apache.tapestry5.services.javascript.JavaScriptSupport; +import org.apache.tapestry5.services.javascript.ModuleConfigurationCallback; +import org.apache.tapestry5.services.javascript.StylesheetLink; public class JavaScriptSupportImpl implements JavaScriptSupport { @@ -47,6 +59,10 @@ public class JavaScriptSupportImpl implements JavaScriptSupport private final List<StylesheetLink> stylesheetLinks = CollectionFactory.newList(); private final List<InitializationImpl> inits = CollectionFactory.newList(); + + private final List<EsModuleInitialization> esModuleInits = CollectionFactory.newList(); + + private final Set<String> esModulesImported = CollectionFactory.newSet(); private final JavaScriptStackSource javascriptStackSource; @@ -62,47 +78,6 @@ public class JavaScriptSupportImpl implements JavaScriptSupport private Map<String, String> libraryURLToStackName, moduleNameToStackName; - class InitializationImpl implements Initialization - { - InitializationPriority priority = InitializationPriority.NORMAL; - - final String moduleName; - - String functionName; - - JSONArray arguments; - - InitializationImpl(String moduleName) - { - this.moduleName = moduleName; - } - - public Initialization invoke(String functionName) - { - assert InternalUtils.isNonBlank(functionName); - - this.functionName = functionName; - - return this; - } - - public Initialization priority(InitializationPriority priority) - { - assert priority != null; - - this.priority = priority; - - return this; - } - - public void with(Object... arguments) - { - assert arguments != null; - - this.arguments = new JSONArray(arguments); - } - } - public JavaScriptSupportImpl(DocumentLinker linker, JavaScriptStackSource javascriptStackSource, JavaScriptStackPathConstructor stackPathConstructor, BooleanHook suppressCoreStylesheetsHook) { @@ -150,6 +125,8 @@ public class JavaScriptSupportImpl implements JavaScriptSupport public void commit() { + + // TODO make no Require.js version of this if (focusFieldId != null) { require("t5/core/pageinit").invoke("focus").with(focusFieldId); @@ -176,6 +153,11 @@ public class JavaScriptSupportImpl implements JavaScriptSupport linker.addInitialization(element.priority, element.moduleName, element.functionName, element.arguments); } }); + + if (!esModuleInits.isEmpty()) + { + esModuleInits.stream().forEach(linker::addEsModuleInitialization); + } } public void addInitializerCall(InitializationPriority priority, String functionName, JSONObject parameter) @@ -462,4 +444,27 @@ public class JavaScriptSupportImpl implements JavaScriptSupport return init; } + @Override + public EsModuleInitialization importEsModule(String moduleName) + { + + assert InternalUtils.isNonBlank(moduleName); + + // TODO import core libraries (jQuery, Prototype/Scriptaculous/Underscore) + + EsModuleInitialization init = new EsModuleInitializationImpl(moduleName); + if (!esModulesImported.contains(moduleName)) + { + esModuleInits.add(init); + } + + return init; + } + + @Override + public void addEsModuleConfigurationCallback(EsModuleConfigurationCallback callback) + { + linker.addEsModuleConfigurationCallback(callback); + } + } diff --git a/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/assets/ResourceChangeTrackerImpl.java b/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/assets/ResourceChangeTrackerImpl.java index 73d4d2644..54e3bc485 100644 --- a/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/assets/ResourceChangeTrackerImpl.java +++ b/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/assets/ResourceChangeTrackerImpl.java @@ -92,12 +92,11 @@ public class ResourceChangeTrackerImpl extends InvalidationEventHubImpl implemen public void forceInvalidationEvent() { - fireInvalidationEvent(); - if (tracker != null) { tracker.clear(); } + fireInvalidationEvent(); } public void checkForUpdates() diff --git a/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/javascript/EsModuleManagerImpl.java b/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/javascript/EsModuleManagerImpl.java new file mode 100644 index 000000000..9f9bf5aea --- /dev/null +++ b/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/javascript/EsModuleManagerImpl.java @@ -0,0 +1,345 @@ +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package org.apache.tapestry5.internal.services.javascript; + +import java.io.IOException; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.apache.tapestry5.Asset; +import org.apache.tapestry5.SymbolConstants; +import org.apache.tapestry5.commons.util.AvailableValues; +import org.apache.tapestry5.commons.util.CollectionFactory; +import org.apache.tapestry5.commons.util.UnknownValueException; +import org.apache.tapestry5.dom.Element; +import org.apache.tapestry5.http.TapestryHttpSymbolConstants; +import org.apache.tapestry5.internal.InternalConstants; +import org.apache.tapestry5.internal.services.ajax.EsModuleInitializationImpl; +import org.apache.tapestry5.internal.services.assets.ResourceChangeTracker; +import org.apache.tapestry5.ioc.annotations.PostInjection; +import org.apache.tapestry5.ioc.annotations.Symbol; +import org.apache.tapestry5.ioc.services.ClasspathMatcher; +import org.apache.tapestry5.ioc.services.ClasspathScanner; +import org.apache.tapestry5.json.JSONCollection; +import org.apache.tapestry5.json.JSONLiteral; +import org.apache.tapestry5.json.JSONObject; +import org.apache.tapestry5.services.AssetSource; +import org.apache.tapestry5.services.assets.StreamableResourceSource; +import org.apache.tapestry5.services.javascript.EsModuleConfigurationCallback; +import org.apache.tapestry5.services.javascript.EsModuleInitialization; +import org.apache.tapestry5.services.javascript.EsModuleManager; +import org.apache.tapestry5.services.javascript.ImportPlacement; + +public class EsModuleManagerImpl implements EsModuleManager +{ + + private static final String GENERIC_IMPORTED_VARIABLE = "m"; + + /** + * Name of the JSON object property containing the imports in an import map. + */ + public static final String IMPORTS_ATTRIBUTE = EsModuleConfigurationCallback.IMPORTS_ATTRIBUTE; + + private static final String CLASSPATH_ROOT = "META-INF/assets/es-modules/"; + + private final boolean compactJSON; + + private final boolean productionMode; + + private final Set<String> extensions; + + private final AssetSource assetSource; + + // Note: ConcurrentHashMap does not support null as a value, alas. We use classpathRoot as a null. + private final Map<String, String> cache = CollectionFactory.newConcurrentMap(); + + private final ClasspathScanner classpathScanner; + + private JSONObject importMap; + + private final ResourceChangeTracker resourceChangeTracker; + + private final List<EsModuleConfigurationCallback> globalCallbacks; + + public EsModuleManagerImpl( + List<EsModuleConfigurationCallback> globalCallbacks, + AssetSource assetSource, + StreamableResourceSource streamableResourceSource, + @Symbol(SymbolConstants.COMPACT_JSON) + boolean compactJSON, + @Symbol(TapestryHttpSymbolConstants.PRODUCTION_MODE) + boolean productionMode, + ClasspathScanner classpathScanner, + ResourceChangeTracker resourceChangeTracker) + { + this.compactJSON = compactJSON; + this.assetSource = assetSource; + this.classpathScanner = classpathScanner; + this.globalCallbacks = globalCallbacks; + this.productionMode = productionMode; + this.resourceChangeTracker = resourceChangeTracker; + importMap = new JSONObject(); + + extensions = CollectionFactory.newSet("js"); + extensions.addAll(streamableResourceSource.fileExtensionsForContentType(InternalConstants.JAVASCRIPT_CONTENT_TYPE)); + + createImportMap(); + + } + + private void createImportMap() + { + + JSONObject importMap = new JSONObject(); + JSONObject imports = importMap.in(IMPORTS_ATTRIBUTE); + + resourceChangeTracker.addInvalidationCallback(this::createImportMap); + cache.clear(); + + loadBaseModuleList(imports); + + for (String name : cache.keySet()) + { + imports.put(name, cache.get(name)); + } + + this.importMap = executeCallbacks(importMap, globalCallbacks); + + for (String id : imports.keySet()) + { + cache.put(id, imports.getString(id)); + } + + } + + private void loadBaseModuleList(JSONObject imports) + { + ClasspathMatcher matcher = (packagePath, fileName) -> + extensions.stream().anyMatch(e -> fileName.endsWith(e)); + try + { + final Set<String> scan = classpathScanner.scan(CLASSPATH_ROOT, matcher); + for (String file : scan) + { + String id = file.replace(CLASSPATH_ROOT, ""); + id = id.substring(0, id.lastIndexOf('.')); + + final Asset asset = assetSource.getClasspathAsset(file); + resourceChangeTracker.trackResource(asset.getResource()); + imports.put(id, asset.toClientURL()); + } + } catch (IOException e) + { + throw new RuntimeException(e); + } + } + + @PostInjection + public void setupInvalidation(ResourceChangeTracker tracker) + { + + } + + @Override + public void writeImportMap(Element head, List<EsModuleConfigurationCallback> moduleConfigurationCallbacks) { + + // Cloning the original import map JSON object + final JSONObject imports = ((JSONObject) importMap.get(EsModuleConfigurationCallback.IMPORTS_ATTRIBUTE)) + .copy(); + JSONObject newImportMap = new JSONObject( + EsModuleConfigurationCallback.IMPORTS_ATTRIBUTE, imports); + + newImportMap = executeCallbacks(newImportMap, moduleConfigurationCallbacks); + + head.element("script") + .attribute("type", "importmap") + .text(newImportMap.toString(compactJSON)); + } + + @Override + public void writeImports(Element root, List<EsModuleInitialization> inits) + { + Element script; + Element body = null; + Element head = null; + ImportPlacement placement; + EsModuleInitializationImpl init; + String functionName; + Object[] arguments; + + for (EsModuleInitialization i : inits) + { + + init = (EsModuleInitializationImpl) i; + final String moduleId = init.getModuleId(); + // Making sure the user doesn't shoot heir own foot + final String url = cache.get(moduleId); + if (url == null) + { + throw new UnknownValueException("ES module not found: " + moduleId, + new AvailableValues("String", cache)); + } + + placement = init.getPlacement(); + if (placement.equals(ImportPlacement.HEAD)) + { + if (head == null) + { + head = root.find("head"); + } + script = head.element("script"); + } + else { + if (body == null) + { + body = root.find("body"); + } + if (placement.equals(ImportPlacement.BODY_BOTTOM)) { + script = body.element("script"); + } + else if (placement.equals(ImportPlacement.BODY_TOP)) + { + script = body.elementAt(0, "script"); + } + else + { + throw new IllegalArgumentException("Unknown import placement: " + placement); + } + } + + writeAttributes(script, init); + script.attribute("src", url); + + functionName = init.getFunctionName(); + arguments = init.getArguments(); + + if (!productionMode) + { + script.attribute("data-module-id", moduleId); + final Element log = script.element("script", "type", "text/javascript"); + log.text(String.format("console.debug('Imported ES module %s');", moduleId)); + log.moveBefore(script); + } + + // If we have not only the import, but also an automatic function call + if (arguments != null || functionName != null) + { + final Element moduleFunctionCall = script.element("script"); + + moduleFunctionCall.moveAfter(script); + + final String moduleFunctionCallFormat = + "import %s from '%s';\n" + + "%s(%s);"; + + final String importName = functionName != null ? functionName : GENERIC_IMPORTED_VARIABLE; + final String importDeclaration = functionName != null ? + "{ " + functionName + " }": + GENERIC_IMPORTED_VARIABLE; + + moduleFunctionCall.text(String.format(moduleFunctionCallFormat, + importDeclaration, moduleId, importName, + convertToJsFunctionParameters(arguments, compactJSON))); + + writeAttributes(moduleFunctionCall, init); + + // Avoiding duplicated ids + final String id = moduleFunctionCall.getAttribute("id"); + if (id != null) + { + moduleFunctionCall.forceAttributes("id", id + "-function-call"); + } + + } + + } + + } + + static String convertToJsFunctionParameters(Object[] arguments, boolean compactJSON) + { + String result; + if (arguments == null || arguments.length == 0) + { + result = ""; + } + else if (arguments.length == 1) + { + result = convertToJsFunctionParameter(arguments[0], compactJSON); + } + else { + StringBuilder builder = new StringBuilder(); + for (int i = 0; i < arguments.length; i++) + { + if (i > 0) + { + builder.append(", "); + } + builder.append(convertToJsFunctionParameter(arguments[i], compactJSON)); + } + result = builder.toString(); + } + return result; + } + + static String convertToJsFunctionParameter(Object object, boolean compactJSON) + { + String result; + + if (object == null) + { + result = null; + } + else if (object instanceof String || object instanceof JSONLiteral) + { + result = "'" + object.toString() + "'"; + } + else if (object instanceof Number || object instanceof Boolean) + { + result = object.toString(); + } + else if (object instanceof JSONCollection) + { + result = ((JSONCollection) object).toString(compactJSON); + } + else + { + throw new IllegalArgumentException(String.format( + "Unsupported value: %s (type %s)", object.toString(), object.getClass().getName())); + } + + return result; + } + + private void writeAttributes(Element script, EsModuleInitializationImpl init) { + final Map<String, String> attributes = init.getAttributes(); + for (String name : attributes.keySet()) + { + script.attribute(name, attributes.get(name)); + } + + script.attribute("type", "module"); + } + + private JSONObject executeCallbacks(JSONObject importMap, List<EsModuleConfigurationCallback> callbacks) + { + for (EsModuleConfigurationCallback callback : callbacks) + { + callback.configure(importMap); + } + + return importMap; + } + +} diff --git a/tapestry-core/src/main/java/org/apache/tapestry5/internal/transform/ImportWorker.java b/tapestry-core/src/main/java/org/apache/tapestry5/internal/transform/ImportWorker.java index 36130ddaf..cc93a7b95 100644 --- a/tapestry-core/src/main/java/org/apache/tapestry5/internal/transform/ImportWorker.java +++ b/tapestry-core/src/main/java/org/apache/tapestry5/internal/transform/ImportWorker.java @@ -12,6 +12,11 @@ package org.apache.tapestry5.internal.transform; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + import org.apache.tapestry5.Asset; import org.apache.tapestry5.ComponentResources; import org.apache.tapestry5.SymbolConstants; @@ -23,8 +28,18 @@ import org.apache.tapestry5.func.Worker; import org.apache.tapestry5.internal.services.assets.ResourceChangeTracker; import org.apache.tapestry5.ioc.services.SymbolSource; import org.apache.tapestry5.model.MutableComponentModel; -import org.apache.tapestry5.plastic.*; +import org.apache.tapestry5.plastic.ComputedValue; +import org.apache.tapestry5.plastic.FieldHandle; +import org.apache.tapestry5.plastic.InstanceContext; +import org.apache.tapestry5.plastic.MethodAdvice; +import org.apache.tapestry5.plastic.MethodInvocation; +import org.apache.tapestry5.plastic.PlasticClass; +import org.apache.tapestry5.plastic.PlasticField; +import org.apache.tapestry5.plastic.PlasticMethod; +import org.apache.tapestry5.plastic.PlasticUtils; import org.apache.tapestry5.plastic.PlasticUtils.FieldInfo; +import org.apache.tapestry5.plastic.PropertyAccessType; +import org.apache.tapestry5.plastic.PropertyValueProvider; import org.apache.tapestry5.services.AssetSource; import org.apache.tapestry5.services.TransformConstants; import org.apache.tapestry5.services.javascript.Initialization; @@ -32,11 +47,6 @@ import org.apache.tapestry5.services.javascript.JavaScriptSupport; import org.apache.tapestry5.services.transform.ComponentClassTransformWorker2; import org.apache.tapestry5.services.transform.TransformationSupport; -import java.util.ArrayList; -import java.util.HashSet; -import java.util.List; -import java.util.Set; - /** * Implements the {@link Import} annotation, both at the class and at the method level. * @@ -154,6 +164,8 @@ public class ImportWorker implements ComponentClassTransformWorker2 importStylesheets(componentClass, model, method, annotation.stylesheet(), fieldInfos); importModules(method, annotation.module()); + + importEsModules(method, annotation.esModule()); } private void importStacks(PlasticMethod method, String[] stacks) @@ -179,6 +191,14 @@ public class ImportWorker implements ComponentClassTransformWorker2 } }; } + + private void importEsModules(PlasticMethod method, String[] moduleIds) + { + if (moduleIds.length != 0) + { + method.addAdvice(createImportEsModulesAdvice(moduleIds)); + } + } private void importModules(PlasticMethod method, String[] moduleNames) { @@ -208,6 +228,22 @@ public class ImportWorker implements ComponentClassTransformWorker2 } } } + + private MethodAdvice createImportEsModulesAdvice(final String[] moduleIds) + { + return new MethodAdvice() + { + public void advise(MethodInvocation invocation) + { + for (String moduleId : moduleIds) + { + javascriptSupport.importEsModule(moduleId); + } + + invocation.proceed(); + } + }; + } private MethodAdvice createImportModulesAdvice(final String[] moduleNames) { diff --git a/tapestry-core/src/main/java/org/apache/tapestry5/modules/JavaScriptModule.java b/tapestry-core/src/main/java/org/apache/tapestry5/modules/JavaScriptModule.java index 955d964fe..b452932f5 100644 --- a/tapestry-core/src/main/java/org/apache/tapestry5/modules/JavaScriptModule.java +++ b/tapestry-core/src/main/java/org/apache/tapestry5/modules/JavaScriptModule.java @@ -32,6 +32,7 @@ import org.apache.tapestry5.internal.services.ajax.JavaScriptSupportImpl; import org.apache.tapestry5.internal.services.assets.ResourceChangeTracker; import org.apache.tapestry5.internal.services.javascript.AddBrowserCompatibilityStyles; import org.apache.tapestry5.internal.services.javascript.ConfigureHTMLElementFilter; +import org.apache.tapestry5.internal.services.javascript.EsModuleManagerImpl; import org.apache.tapestry5.internal.services.javascript.Internal; import org.apache.tapestry5.internal.services.javascript.JavaScriptStackPathConstructor; import org.apache.tapestry5.internal.services.javascript.JavaScriptStackSourceImpl; @@ -60,6 +61,7 @@ import org.apache.tapestry5.services.PathConstructor; import org.apache.tapestry5.services.compatibility.Compatibility; import org.apache.tapestry5.services.compatibility.Trait; import org.apache.tapestry5.services.javascript.AMDWrapper; +import org.apache.tapestry5.services.javascript.EsModuleManager; import org.apache.tapestry5.services.javascript.ExtensibleJavaScriptStack; import org.apache.tapestry5.services.javascript.JavaScriptModuleConfiguration; import org.apache.tapestry5.services.javascript.JavaScriptStack; @@ -92,6 +94,7 @@ public class JavaScriptModule public static void bind(ServiceBinder binder) { binder.bind(ModuleManager.class, ModuleManagerImpl.class); + binder.bind(EsModuleManager.class, EsModuleManagerImpl.class); binder.bind(JavaScriptStackSource.class, JavaScriptStackSourceImpl.class); binder.bind(JavaScriptStack.class, ExtensibleJavaScriptStack.class).withMarker(Core.class).withId("CoreJavaScriptStack"); binder.bind(JavaScriptStack.class, ExtensibleJavaScriptStack.class).withMarker(Internal.class).withId("InternalJavaScriptStack"); @@ -490,6 +493,7 @@ public class JavaScriptModule { configuration.add(SymbolConstants.JAVASCRIPT_INFRASTRUCTURE_PROVIDER, "prototype"); configuration.add(SymbolConstants.MODULE_PATH_PREFIX, "modules"); + configuration.add(SymbolConstants.ES_MODULE_PATH_PREFIX, "es-modules"); } @Contribute(ModuleManager.class) diff --git a/tapestry-core/src/main/java/org/apache/tapestry5/modules/TapestryModule.java b/tapestry-core/src/main/java/org/apache/tapestry5/modules/TapestryModule.java index e3c47e86f..7f339ee65 100644 --- a/tapestry-core/src/main/java/org/apache/tapestry5/modules/TapestryModule.java +++ b/tapestry-core/src/main/java/org/apache/tapestry5/modules/TapestryModule.java @@ -20,19 +20,15 @@ import java.math.BigInteger; import java.net.URL; import java.text.DateFormat; import java.text.SimpleDateFormat; -import java.util.Arrays; import java.util.Calendar; import java.util.Collection; -import java.util.Collections; import java.util.Date; -import java.util.HashSet; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Properties; import java.util.Set; import java.util.regex.Pattern; -import java.util.stream.Collectors; import org.apache.tapestry5.Asset; import org.apache.tapestry5.BindingConstants; @@ -108,12 +104,7 @@ import org.apache.tapestry5.commons.services.TypeCoercer; import org.apache.tapestry5.commons.util.AvailableValues; import org.apache.tapestry5.commons.util.CollectionFactory; import org.apache.tapestry5.commons.util.StrategyRegistry; -import org.apache.tapestry5.corelib.components.BeanEditor; -import org.apache.tapestry5.corelib.components.PropertyDisplay; -import org.apache.tapestry5.corelib.components.PropertyEditor; import org.apache.tapestry5.corelib.data.SecureOption; -import org.apache.tapestry5.corelib.pages.PropertyDisplayBlocks; -import org.apache.tapestry5.corelib.pages.PropertyEditBlocks; import org.apache.tapestry5.grid.GridConstants; import org.apache.tapestry5.grid.GridDataSource; import org.apache.tapestry5.http.Link; @@ -174,7 +165,6 @@ import org.apache.tapestry5.internal.services.*; import org.apache.tapestry5.internal.services.ajax.AjaxFormUpdateFilter; import org.apache.tapestry5.internal.services.ajax.AjaxResponseRendererImpl; import org.apache.tapestry5.internal.services.ajax.MultiZoneUpdateEventResultProcessor; -import org.apache.tapestry5.internal.services.assets.ResourceChangeTracker; import org.apache.tapestry5.internal.services.exceptions.ExceptionReportWriterImpl; import org.apache.tapestry5.internal.services.exceptions.ExceptionReporterImpl; import org.apache.tapestry5.internal.services.linktransform.LinkTransformerImpl; @@ -365,6 +355,7 @@ import org.apache.tapestry5.services.ValueLabelProvider; import org.apache.tapestry5.services.ajax.AjaxResponseRenderer; import org.apache.tapestry5.services.dynamic.DynamicTemplate; import org.apache.tapestry5.services.dynamic.DynamicTemplateParser; +import org.apache.tapestry5.services.javascript.EsModuleManager; import org.apache.tapestry5.services.javascript.JavaScriptSupport; import org.apache.tapestry5.services.javascript.ModuleManager; import org.apache.tapestry5.services.linktransform.ComponentEventLinkTransformer; @@ -376,7 +367,6 @@ import org.apache.tapestry5.services.meta.FixedExtractor; import org.apache.tapestry5.services.meta.MetaDataExtractor; import org.apache.tapestry5.services.meta.MetaWorker; import org.apache.tapestry5.services.pageload.PageClassLoaderContextManager; -import org.apache.tapestry5.services.pageload.PageClassLoaderContextManagerImpl; import org.apache.tapestry5.services.pageload.PreloaderMode; import org.apache.tapestry5.services.rest.MappedEntityManager; import org.apache.tapestry5.services.rest.OpenApiDescriptionGenerator; @@ -1803,6 +1793,8 @@ public final class TapestryModule public void contributeMarkupRenderer(OrderedConfiguration<MarkupRendererFilter> configuration, final ModuleManager moduleManager, + + final EsModuleManager esModuleManager, @Symbol(SymbolConstants.OMIT_GENERATOR_META) final boolean omitGeneratorMeta, @@ -1825,7 +1817,7 @@ public final class TapestryModule { public void renderMarkup(MarkupWriter writer, MarkupRenderer renderer) { - DocumentLinkerImpl linker = new DocumentLinkerImpl(moduleManager, omitGeneratorMeta, enablePageloadingMask, tapestryVersion); + DocumentLinkerImpl linker = new DocumentLinkerImpl(moduleManager, esModuleManager, omitGeneratorMeta, enablePageloadingMask, tapestryVersion); environment.push(DocumentLinker.class, linker); diff --git a/tapestry-core/src/main/java/org/apache/tapestry5/services/javascript/Initialization.java b/tapestry-core/src/main/java/org/apache/tapestry5/services/javascript/AbstractInitialization.java similarity index 72% copy from tapestry-core/src/main/java/org/apache/tapestry5/services/javascript/Initialization.java copy to tapestry-core/src/main/java/org/apache/tapestry5/services/javascript/AbstractInitialization.java index a633722fc..6aa58d866 100644 --- a/tapestry-core/src/main/java/org/apache/tapestry5/services/javascript/Initialization.java +++ b/tapestry-core/src/main/java/org/apache/tapestry5/services/javascript/AbstractInitialization.java @@ -13,12 +13,11 @@ package org.apache.tapestry5.services.javascript; /** - * Provided by {@link JavaScriptSupport#require(String)} to allow additional, optional, details of the module-based page initialization - * to be configured. + * Superinterface with the parts shared by {@linkplain Initialization} and {@linkplain EsModuleInitialization}. * - * @since 5.4 + * @since 5.10.0 */ -public interface Initialization +public interface AbstractInitialization<T extends AbstractInitialization<?>> { /** @@ -29,18 +28,7 @@ public interface Initialization * name of a function exported by the module. * @return this Initialization, for further configuration */ - Initialization invoke(String functionName); - - /** - * Changes the initialization priority of the initialization from its default, {@link InitializationPriority#NORMAL}. - * - * Note: it is possible that this method may be removed before release 5.4 is final. - * - * @param priority - * new priority - * @return this Initialization, for further configuration - */ - Initialization priority(InitializationPriority priority); + T invoke(String functionName); /** * Specifies the arguments to be passed to the function. Often, just a single {@link org.apache.tapestry5.json.JSONObject} diff --git a/tapestry-core/src/main/java/org/apache/tapestry5/services/javascript/EsModuleConfigurationCallback.java b/tapestry-core/src/main/java/org/apache/tapestry5/services/javascript/EsModuleConfigurationCallback.java new file mode 100644 index 000000000..eb0e6adf9 --- /dev/null +++ b/tapestry-core/src/main/java/org/apache/tapestry5/services/javascript/EsModuleConfigurationCallback.java @@ -0,0 +1,62 @@ +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package org.apache.tapestry5.services.javascript; + +import org.apache.tapestry5.json.JSONObject; + +/** + * Interface used to to change the JSON configuration object which will be used in the + * import map to be generated by the {@linkplain ModuleManager} service at 2 different times: + * <ol> + * <li> + * During webapp, based on on contributions to {@linkplain EsModuleManager}. + * These are considered global callbacks, since they affect the base + * import map used in all requests. + * </li> + * <li> + * During page rendering, allowing components, pages and base components + * to further customize the base import map by for that specific request in + * a per-request basis by using the + * {@linkplain JavaScriptSupport#addEsModuleConfigurationCallback(EsModuleConfigurationCallback)} method. + * </li> + * </ol> + * + * @see JavaScriptSupport#addEsModuleConfigurationCallback(EsModuleConfigurationCallback) + * @since 5.10.0 + */ +public interface EsModuleConfigurationCallback +{ + /** + * Name of the JSON object property containing the imports in an import map. + */ + String IMPORTS_ATTRIBUTE = "imports"; + + /** + * Receives the current configuration, which can be copied or returned, or, more typically, modified and returned. + * + * @param configuration + * a {@link JSONObject} containing the current configuration. + */ + void configure(JSONObject configuration); + + /** + * Utility method to set or override a module and its URL. + * @param object the {@link JSONObject}. + * @param id the module id. + * @param url the module URL. + */ + public static void setImport(JSONObject object, String id, String url) + { + object.in(IMPORTS_ATTRIBUTE).put(id, url); + } +} diff --git a/tapestry-core/src/main/java/org/apache/tapestry5/services/javascript/EsModuleInitialization.java b/tapestry-core/src/main/java/org/apache/tapestry5/services/javascript/EsModuleInitialization.java new file mode 100644 index 000000000..f56479398 --- /dev/null +++ b/tapestry-core/src/main/java/org/apache/tapestry5/services/javascript/EsModuleInitialization.java @@ -0,0 +1,96 @@ +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package org.apache.tapestry5.services.javascript; + +import java.util.Map; + +/** + * Provided by {@link JavaScriptSupport#importEsModule(String)} to allow additional, optional, + * details of the ES module import. + * + * @since 5.10.0 + */ +public interface EsModuleInitialization extends AbstractInitialization<EsModuleInitialization> +{ + + /** + * Defines an attribute name and value to be added to the corresponding + * <code><script></code> element. If the attribute was already set, + * its value will be overwritten. + * + * @param name The attribute name. It cannot be null nor empty. + * @param value The attribute value. It cannot be null nor empty. + * @return this <code>EsModuleInitialization</code> for further configuration. + */ + EsModuleInitialization withAttribute(String name, String value); + + /** + * Same as <code>withAttribute(name, name)</code>. Useful for attributes + * without values, such as <code>defer</code> and <code>async</code>. + * + * @param name The attribute name. It cannot be null nor empty. + * @return this <code>EsModuleInitialization</code> for further configuration. + */ + default EsModuleInitialization withAttribute(String name) + { + return withAttribute(name, name); + } + + /** + * Utility method for adding the <code>defer</code> attribute. + * @return this <code>EsModuleInitialization</code> for further configuration. + */ + default EsModuleInitialization withDefer() + { + return withAttribute("defer"); + } + + /** + * Utility method for adding the <code>async</code> attribute. + * @return this <code>EsModuleInitialization</code> for further configuration. + */ + default EsModuleInitialization withAsync() + { + return withAttribute("async"); + } + + /** + * Defines where this import should be done. + * @param placement an {@linkplain ImportPlacement} instance. It cannot be null. + * @return this <code>EsModuleInitialization</code> for further configuration. + */ + EsModuleInitialization placement(ImportPlacement placement); + + /** + * Specifies the name of an module exported function to invoke. + * If this method is not invoked, then the module is expected to export + * just a single function (which may, or may not, take {@linkplain #with(Object...) parameters}). + * + * @param functionName + * name of a function exported by the module. + * @return this <code>EsModuleInitialization</code>, for further configuration. + */ + EsModuleInitialization invoke(String functionName); + + /** + * Specifies the arguments to be passed to the function. Often, just a single {@link org.apache.tapestry5.json.JSONObject} + * is passed. + * + * @param arguments + * any number of values. Each value may be one of: null, String, Boolean, Number, + * {@link org.apache.tapestry5.json.JSONObject}, {@link org.apache.tapestry5.json.JSONArray}, or + * {@link org.apache.tapestry5.json.JSONLiteral}. + */ + void with(Object... arguments); + +} diff --git a/tapestry-core/src/main/java/org/apache/tapestry5/services/javascript/EsModuleManager.java b/tapestry-core/src/main/java/org/apache/tapestry5/services/javascript/EsModuleManager.java new file mode 100644 index 000000000..19cfec30b --- /dev/null +++ b/tapestry-core/src/main/java/org/apache/tapestry5/services/javascript/EsModuleManager.java @@ -0,0 +1,57 @@ +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package org.apache.tapestry5.services.javascript; + +import java.util.List; + +import org.apache.tapestry5.dom.Element; +import org.apache.tapestry5.ioc.annotations.UsesOrderedConfiguration; + +/** + * Responsible for managing access to the ES modules. This service's distributed + * configuration allows the initial import map JSON object to be customized + * in a webapp-wide basis. + * + * @since 5.10.0 + * @see EsModuleConfigurationCallback + */ +@UsesOrderedConfiguration(EsModuleConfigurationCallback.class) +public interface EsModuleManager +{ + /** + * Invoked by the internal {@link org.apache.tapestry5.internal.services.DocumentLinker} service to + * write the import map into the page. + * + * @param head + * {@code <body>} element of the page, to which new {@code <script>} element(s) may be added. + * @param moduleConfigurationCallbacks + * a list of {@link org.apache.tapestry5.services.javascript.ModuleConfigurationCallback}s, which + * is used to customize the configuration before it is written. + */ + void writeImportMap(Element head, + List<EsModuleConfigurationCallback> moduleConfigurationCallbacks); + + /** + * Invoked by the internal {@link org.apache.tapestry5.internal.services.DocumentLinker} service to write the + * ES module imports (as per {@link JavaScriptSupport#importEsModule(String)} into the page. + * this occurs after the ES module infrastructure + * has been written into the page, along with the core libraries. + * + * @param root + * {@code <root>} element of the page. + * @param inits + * specify initialization on the page, based on loading modules, extacting functions from modules, and invoking those functions + */ + void writeImports(Element root, List<EsModuleInitialization> inits); + +} diff --git a/tapestry-core/src/main/java/org/apache/tapestry5/services/javascript/ImportPlacement.java b/tapestry-core/src/main/java/org/apache/tapestry5/services/javascript/ImportPlacement.java new file mode 100644 index 000000000..c53585ffd --- /dev/null +++ b/tapestry-core/src/main/java/org/apache/tapestry5/services/javascript/ImportPlacement.java @@ -0,0 +1,37 @@ +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package org.apache.tapestry5.services.javascript; + +/** + * Enumeration class defining the possible placements of JavaScript imports. + * + * @since 5.10.0 + */ +public enum ImportPlacement +{ + /** + * Inside the <code><head></code> HTML element. + */ + HEAD, + + /** + * Towards the top of the <code><body></code> HTML element. + */ + BODY_TOP, + + /** + * Towards the bottom of the <code><body></code> HTML element. + */ + BODY_BOTTOM + +} diff --git a/tapestry-core/src/main/java/org/apache/tapestry5/services/javascript/Initialization.java b/tapestry-core/src/main/java/org/apache/tapestry5/services/javascript/Initialization.java index a633722fc..6f7e414e5 100644 --- a/tapestry-core/src/main/java/org/apache/tapestry5/services/javascript/Initialization.java +++ b/tapestry-core/src/main/java/org/apache/tapestry5/services/javascript/Initialization.java @@ -18,7 +18,7 @@ package org.apache.tapestry5.services.javascript; * * @since 5.4 */ -public interface Initialization +public interface Initialization extends AbstractInitialization<Initialization> { /** diff --git a/tapestry-core/src/main/java/org/apache/tapestry5/services/javascript/JavaScriptSupport.java b/tapestry-core/src/main/java/org/apache/tapestry5/services/javascript/JavaScriptSupport.java index 526144da1..7624497b0 100644 --- a/tapestry-core/src/main/java/org/apache/tapestry5/services/javascript/JavaScriptSupport.java +++ b/tapestry-core/src/main/java/org/apache/tapestry5/services/javascript/JavaScriptSupport.java @@ -273,4 +273,23 @@ public interface JavaScriptSupport */ void addModuleConfigurationCallback(ModuleConfigurationCallback callback); + /** + * Imports an <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Modules">ES module</a>. + * @param moduleId the id of the module to import. + * @return an <code>EsModuleInitialization</code> instance to optionally configure + * the import and add a call an exported function, with or without parameters. + * @since 5.10.0 + */ + EsModuleInitialization importEsModule(String moduleId); + + /** + * Adds an ES module configuration callback for this request. + * + * @param callback + * a {@link ModuleConfigurationCallback}. It cannot be null. + * @see DocumentLinker#addModuleConfigurationCallback(ModuleConfigurationCallback) + * @since 5.10.0 + */ + void addEsModuleConfigurationCallback(EsModuleConfigurationCallback callback); + } diff --git a/tapestry-core/src/main/java/org/apache/tapestry5/services/javascript/ModuleConfigurationCallback.java b/tapestry-core/src/main/java/org/apache/tapestry5/services/javascript/ModuleConfigurationCallback.java index 79c5d00f8..790aaef2d 100644 --- a/tapestry-core/src/main/java/org/apache/tapestry5/services/javascript/ModuleConfigurationCallback.java +++ b/tapestry-core/src/main/java/org/apache/tapestry5/services/javascript/ModuleConfigurationCallback.java @@ -33,7 +33,7 @@ import org.apache.tapestry5.json.JSONObject; public interface ModuleConfigurationCallback { /** - * Receives the current configuration, which can be copied or returned, or (more typically) modified and returned. + * Receives the current configuration, which can be copied or returned, or, more typically, modified and returned. * * @param configuration * a {@link JSONObject} containing the current configuration. diff --git a/tapestry-core/src/test/groovy/org/apache/tapestry5/internal/services/DocumentLinkerImplTest.groovy b/tapestry-core/src/test/groovy/org/apache/tapestry5/internal/services/DocumentLinkerImplTest.groovy index 11c1b4e55..a9eafe7a7 100644 --- a/tapestry-core/src/test/groovy/org/apache/tapestry5/internal/services/DocumentLinkerImplTest.groovy +++ b/tapestry-core/src/test/groovy/org/apache/tapestry5/internal/services/DocumentLinkerImplTest.groovy @@ -32,7 +32,7 @@ class DocumentLinkerImplTest extends InternalBaseTestCase { document.newRootElement("not-html").text("not an HTML document") - DocumentLinkerImpl linker = new DocumentLinkerImpl(null, true, false, "1.2.3") + DocumentLinkerImpl linker = new DocumentLinkerImpl(null, null, true, false, "1.2.3") // Only checked if there's something to link. @@ -55,7 +55,7 @@ class DocumentLinkerImplTest extends InternalBaseTestCase { document.newRootElement("not-html").text("not an HTML document") - DocumentLinkerImpl linker = new DocumentLinkerImpl(null, true, false, "1.2.3") + DocumentLinkerImpl linker = new DocumentLinkerImpl(null, null, true, false, "1.2.3") // Only checked if there's something to link. @@ -76,7 +76,7 @@ class DocumentLinkerImplTest extends InternalBaseTestCase { void missing_root_element_is_a_noop() { Document document = new Document() - DocumentLinkerImpl linker = new DocumentLinkerImpl(null, true, false, "1.2.3") + DocumentLinkerImpl linker = new DocumentLinkerImpl(null, null, true, false, "1.2.3") linker.addLibrary("foo.js") linker.addScript(InitializationPriority.NORMAL, "doSomething();") @@ -94,7 +94,7 @@ class DocumentLinkerImplTest extends InternalBaseTestCase { def manager = mockModuleManager(["core.js", "foo.js", "bar/baz.js"], [new JSONArray("t5/core/pageinit:evalJavaScript", "pageINIT();")]) - DocumentLinkerImpl linker = new DocumentLinkerImpl(manager, true, false, "1.2.3") + DocumentLinkerImpl linker = new DocumentLinkerImpl(manager, null, true, false, "1.2.3") replay() @@ -122,7 +122,7 @@ class DocumentLinkerImplTest extends InternalBaseTestCase { document.newRootElement("html").element("body").element("p").text("Ready to be marked with generator meta.") - DocumentLinkerImpl linker = new DocumentLinkerImpl(null, false, false, "1.2.3") + DocumentLinkerImpl linker = new DocumentLinkerImpl(null, null, false, false, "1.2.3") linker.updateDocument(document) @@ -141,7 +141,7 @@ class DocumentLinkerImplTest extends InternalBaseTestCase { document.newRootElement("no_html").text("Generator meta only added if root is html tag.") - DocumentLinkerImpl linker = new DocumentLinkerImpl(null, false, false, "1.2.3") + DocumentLinkerImpl linker = new DocumentLinkerImpl(null, null, false, false, "1.2.3") linker.updateDocument(document) @@ -158,7 +158,7 @@ class DocumentLinkerImplTest extends InternalBaseTestCase { document.newRootElement("html").element("body").element("p").text("Ready to be updated with styles.") - DocumentLinkerImpl linker = new DocumentLinkerImpl(null, true, false, "1.2.3") + DocumentLinkerImpl linker = new DocumentLinkerImpl(null, null, true, false, "1.2.3") linker.addStylesheetLink(new StylesheetLink("foo.css")) linker.addStylesheetLink(new StylesheetLink("bar/baz.css", new StylesheetOptions("print"))) @@ -178,7 +178,7 @@ class DocumentLinkerImplTest extends InternalBaseTestCase { document.newRootElement("html").element("head").comment(" existing head ").container.element("body").text( "body content") - DocumentLinkerImpl linker = new DocumentLinkerImpl(null, true, false, "1.2.3") + DocumentLinkerImpl linker = new DocumentLinkerImpl(null, null, true, false, "1.2.3") linker.addStylesheetLink(new StylesheetLink("foo.css")) @@ -198,7 +198,7 @@ class DocumentLinkerImplTest extends InternalBaseTestCase { def manager = mockModuleManager([], [new JSONArray("t5/core/pageinit:evalJavaScript", "doSomething();")]) - DocumentLinkerImpl linker = new DocumentLinkerImpl(manager, true, true, "1.2.3") + DocumentLinkerImpl linker = new DocumentLinkerImpl(manager, null, true, true, "1.2.3") replay() @@ -224,7 +224,7 @@ class DocumentLinkerImplTest extends InternalBaseTestCase { def manager = mockModuleManager(["foo.js"], []) - DocumentLinkerImpl linker = new DocumentLinkerImpl(manager, true, false, "1.2.3") + DocumentLinkerImpl linker = new DocumentLinkerImpl(manager, null, true, false, "1.2.3") replay() @@ -251,7 +251,7 @@ class DocumentLinkerImplTest extends InternalBaseTestCase { def manager = mockModuleManager([], [new JSONArray("['immediate/module:myfunc', {'fred':'barney'}]")]) - DocumentLinkerImpl linker = new DocumentLinkerImpl(manager, true, false, "1.2.3") + DocumentLinkerImpl linker = new DocumentLinkerImpl(manager, null, true, false, "1.2.3") replay() @@ -273,7 +273,7 @@ class DocumentLinkerImplTest extends InternalBaseTestCase { document.newRootElement("html") - DocumentLinkerImpl linker = new DocumentLinkerImpl(null, true, false, "1.2.3") + DocumentLinkerImpl linker = new DocumentLinkerImpl(null, null, true, false, "1.2.3") linker.addStylesheetLink(new StylesheetLink("everybody.css")) linker.addStylesheetLink(new StylesheetLink("just_ie.css", new StylesheetOptions().withCondition("IE"))) @@ -295,7 +295,7 @@ class DocumentLinkerImplTest extends InternalBaseTestCase { document.newRootElement("html") - DocumentLinkerImpl linker = new DocumentLinkerImpl(null, true, false, "1.2.3") + DocumentLinkerImpl linker = new DocumentLinkerImpl(null, null, true, false, "1.2.3") linker.addStylesheetLink(new StylesheetLink("whatever.css")) linker.addStylesheetLink(new StylesheetLink("insertion-point.css", new StylesheetOptions().asAjaxInsertionPoint())) @@ -319,7 +319,7 @@ class DocumentLinkerImplTest extends InternalBaseTestCase { new JSONArray("my/other/module:normal", 111, 222), new JSONArray("my/other/module:late", 333, 444)]) - DocumentLinkerImpl linker = new DocumentLinkerImpl(manager, true, false, "1.2.3") + DocumentLinkerImpl linker = new DocumentLinkerImpl(manager, null, true, false, "1.2.3") replay() @@ -347,7 +347,7 @@ class DocumentLinkerImplTest extends InternalBaseTestCase { def manager = mockModuleManager([], ["my/module", new JSONArray("my/other/module:normal", 111, 222)]) - DocumentLinkerImpl linker = new DocumentLinkerImpl(manager, true, false, "1.2.3") + DocumentLinkerImpl linker = new DocumentLinkerImpl(manager, null, true, false, "1.2.3") replay() diff --git a/tapestry-core/src/test/java/org/apache/tapestry5/integration/app1/EsModuleTests.java b/tapestry-core/src/test/java/org/apache/tapestry5/integration/app1/EsModuleTests.java new file mode 100644 index 000000000..786d9ce0b --- /dev/null +++ b/tapestry-core/src/test/java/org/apache/tapestry5/integration/app1/EsModuleTests.java @@ -0,0 +1,221 @@ +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package org.apache.tapestry5.integration.app1; + +import static org.apache.tapestry5.integration.app1.services.AppModule.NON_OVERRIDDEN_ES_MODULE_ID; +import static org.apache.tapestry5.integration.app1.services.AppModule.NON_OVERRIDDEN_ES_MODULE_URL; +import static org.apache.tapestry5.integration.app1.services.AppModule.OVERRIDDEN_ES_MODULE_ID; +import static org.apache.tapestry5.integration.app1.services.AppModule.OVERRIDDEN_ES_MODULE_NEW_URL; + +import org.apache.tapestry5.annotations.Import; +import org.apache.tapestry5.integration.app1.pages.EsModuleDemo; +import org.apache.tapestry5.internal.transform.ImportWorker; +import org.apache.tapestry5.ioc.annotations.Inject; +import org.apache.tapestry5.json.JSONObject; +import org.apache.tapestry5.services.AssetSource; +import org.apache.tapestry5.services.javascript.EsModuleConfigurationCallback; +import org.apache.tapestry5.services.javascript.EsModuleInitialization; +import org.apache.tapestry5.services.javascript.EsModuleManager; +import org.apache.tapestry5.services.javascript.JavaScriptSupport; +import org.testng.annotations.Test; + +/** + * ES module tests. + */ +public class EsModuleTests extends App1TestCase +{ + private static final String PAGE_NAME = "ES Module Demo"; + + private static final String REQUEST_CALLBACK_SWITCHER = "css=.switch"; + + @Inject + private AssetSource assetSource; + + /** + * Tests whether ES modules placed in /META-INF/es-modules are automatically + * added to import maps. + */ + @Test + public void automatic_modules() + { + openLinks(PAGE_NAME); + JSONObject importMap = getImportMap(); + assertModuleUrlSuffix("foo/bar", "/es-modules/foo/bar.js", importMap); + assertModuleUrlSuffix("root-folder", "/es-modules/root-folder.js", importMap); + } + + /** + * Tests whether ES modules added or overriden through global callbacks + * (i.e. ones contributed to {@link EsModuleManager} configuration) + * are being actually included in the generated import map. + */ + @Test + public void modules_added_by_global_callbacks() + { + openLinks(PAGE_NAME); + JSONObject importMap = getImportMap(); + assertModulesDefinedByGlobalCallbacks(importMap); + } + + /** + * Tests whether ES modules added or overriden through request callbacks + * (i.e. ones added through {@link JavaScriptSupport#addEsModuleConfigurationCallback(EsModuleConfigurationCallback)}) + * are being actually included in the generated import map. + * @throws InterruptedException + */ + @Test + public void modules_added_by_request_callbacks() + { + openLinks(PAGE_NAME); + + // With import map changed by request callbacks. + clickAndWait(REQUEST_CALLBACK_SWITCHER); + JSONObject importMap = getImportMap(); + assertModuleUrl(NON_OVERRIDDEN_ES_MODULE_ID, NON_OVERRIDDEN_ES_MODULE_URL, importMap); + assertModuleUrl(OVERRIDDEN_ES_MODULE_ID, EsModuleDemo.REQUEST_OVERRIDEN_MODULE_URL, importMap); + + // Now without import map changed by request callbacks, so we can test + // the global import map wasn't affected. + clickAndWait(REQUEST_CALLBACK_SWITCHER); + importMap = getImportMap(); + assertModulesDefinedByGlobalCallbacks(importMap); + + } + + /** + * Tests {@link JavaScriptSupport#importEsModule(String)}. + */ + @Test + public void javascript_support_importEsModule() throws InterruptedException + { + + openLinks(PAGE_NAME); + + // Module imported with specified attributes. + assertTrue(isElementPresent("//script[@type='module'][contains(@src, 'foo/bar.js')][@defer='defer'][@async='async'][@something='else'][@foo='foo']")); + + // Module imported with no placement (default body bottom) or + // BODY_BOTTOM should be after the last <div> in this webapp's template + assertTrue(isElementPresent("//body/div[last()][following-sibling::script[@type='module'][contains(@src, '/placement/body-bottom.js')]]")); + + // Module imported with placement BODY_TOP should be before + // the last <div> in this webapp's template (the first one comes from JS). + assertTrue(isElementPresent("//body/div[@role='navigation'][preceding-sibling::script[@type='module'][contains(@src, '/placement/body-top.js')]]")); + + // Module imported with placement HEAD + assertTrue(isElementPresent("//head/script[@type='module'][contains(@src, '/placement/head.js')]")); + + // Checking results of running the modules, not just their inclusion in HTML + assertEquals(getText("message"), "ES module foo/bar imported correctly!"); + assertEquals(getText("head-message"), "ES module imported correctly (<head>)!"); + assertEquals(getText("body-top-message"), "ES module imported correctly (<body> top)!"); + assertEquals(getText("body-bottom-message"), "ES module imported correctly (<body> bottom)!"); + assertEquals(getText("outside-metainf-message"), "ES module correctly imported from outside /META-INF/assets/es-modules!"); + + } + + /** + * Tests importing ES modules through <code>@Import(esModule = ...)</code>. + * @see ImportWorker + * @see Import#esModule() + */ + @Test + public void at_import_esModule() throws InterruptedException + { + openLinks(PAGE_NAME); + assertEquals(getText("root-folder-message"), "ES module imported correctly from the root folder!"); + } + + /** + * Tests using {@link EsModuleInitialization#with(Object...)} without using + * {@link EsModuleInitialization#invoke(String)} (i.e. invoking the default + * exported function with at least one parameter). + */ + @Test + public void invoking_default_exported_function() throws InterruptedException + { + openLinks(PAGE_NAME); + assertEquals( + getText(EsModuleDemo.DEFAULT_EXPORT_MESSAGE), + EsModuleDemo.DEFAULT_EXPORT_PARAMETER); + } + + /** + * Tests using {@link EsModuleInitialization#with(Object...)} without using + * {@link EsModuleInitialization#invoke(String)} (i.e. invoking the default + * exported function). In order words, + * {@code javaScriptSupport.importEsModule("foo").with(...)} + */ + @Test + public void invoking_non_default_exported_function() throws InterruptedException + { + openLinks(PAGE_NAME); + assertEquals( + getText(EsModuleDemo.DEFAULT_EXPORT_MESSAGE), + EsModuleDemo.DEFAULT_EXPORT_PARAMETER); + } + + /** + * Tests using {@code javaScriptSupport.importEsModule("foo").with()} + * (i.e. invoking the default withot parameters) + */ + @Test + public void invoking_non_default_exported_function_without_parameters() throws InterruptedException + { + openLinks(PAGE_NAME); + assertEquals( + getText("parameterless-default-export-message"), + "Parameterless default export!"); + } + + /** + * Tests using whether parameter types are correctly passed to JS. + */ + @Test + public void parameter_types() throws InterruptedException + { + openLinks(PAGE_NAME); + assertEquals( + getText("parameter-type-default-export-message"), + "Parameter types passed correctly!"); + } + + private void assertModulesDefinedByGlobalCallbacks(JSONObject importMap) { + assertModuleUrl(NON_OVERRIDDEN_ES_MODULE_ID, NON_OVERRIDDEN_ES_MODULE_URL, importMap); + assertModuleUrl(OVERRIDDEN_ES_MODULE_ID, OVERRIDDEN_ES_MODULE_NEW_URL, importMap); + } + + private void assertModuleUrlSuffix(String id, String urlSuffix, JSONObject importMap) + { + final JSONObject imports = (JSONObject) importMap.get(EsModuleConfigurationCallback.IMPORTS_ATTRIBUTE); + final String url = imports.getString(id); + + assertNotNull(url, String.format("Module %s not found in import map\n%s", id, importMap.toString(false))); + assertTrue(url.endsWith(urlSuffix), String.format("Unexpected URL %s for module %s (expected %s suffix)", url, id, urlSuffix)); + } + + private void assertModuleUrl(String id, String urlSuffix, JSONObject importMap) + { + final JSONObject imports = (JSONObject) importMap.get(EsModuleConfigurationCallback.IMPORTS_ATTRIBUTE); + final String url = imports.getString(id); + + assertNotNull(url, String.format("Module %s not found in import map\n%s", id, importMap.toString(false))); + assertEquals(url, urlSuffix, String.format("Unexpected URL %s for module %s (expected %s suffix)", url, id, urlSuffix)); + } + + private JSONObject getImportMap() + { + return new JSONObject(getText("import-map-listing")); + } + +} diff --git a/tapestry-core/src/test/java/org/apache/tapestry5/integration/app1/pages/EsModuleDemo.java b/tapestry-core/src/test/java/org/apache/tapestry5/integration/app1/pages/EsModuleDemo.java new file mode 100644 index 000000000..49c38710d --- /dev/null +++ b/tapestry-core/src/test/java/org/apache/tapestry5/integration/app1/pages/EsModuleDemo.java @@ -0,0 +1,107 @@ +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package org.apache.tapestry5.integration.app1.pages; + +import org.apache.tapestry5.annotations.Import; +import org.apache.tapestry5.annotations.Property; +import org.apache.tapestry5.annotations.SetupRender; +import org.apache.tapestry5.integration.app1.services.AppModule; +import org.apache.tapestry5.ioc.annotations.Inject; +import org.apache.tapestry5.json.JSONArray; +import org.apache.tapestry5.json.JSONObject; +import org.apache.tapestry5.services.javascript.EsModuleConfigurationCallback; +import org.apache.tapestry5.services.javascript.ImportPlacement; +import org.apache.tapestry5.services.javascript.JavaScriptSupport; + +@Import(esModule = {"root-folder"}) +public class EsModuleDemo +{ + public static final String DEFAULT_EXPORT_MESSAGE = "default-export-message"; + + public static final String DEFAULT_EXPORT_PARAMETER = "Importing module exporting single function!"; + + public static final String REQUEST_OVERRIDEN_MODULE_URL = "/overridenAgainURL"; + + @Inject + private JavaScriptSupport javaScriptSupport; + + @Property + Boolean overrideEsModuleImportAgain; + + @SetupRender + void importEsModule() + { + // Checking each module is only imported once. + javaScriptSupport.importEsModule("foo/bar") + .withDefer() + .withAsync() + .withAttribute("foo") + .withAttribute("something", "else"); + javaScriptSupport.importEsModule("foo/bar"); + + javaScriptSupport.importEsModule("placement/body-bottom") + .placement(ImportPlacement.BODY_BOTTOM); + javaScriptSupport.importEsModule("placement/body-top") + .placement(ImportPlacement.BODY_TOP); + javaScriptSupport.importEsModule("placement/head") + .placement(ImportPlacement.HEAD); + javaScriptSupport.importEsModule("outside-metainf"); + javaScriptSupport.importEsModule("show-import-map"); + + javaScriptSupport.importEsModule("default-export") + .with(EsModuleDemo.DEFAULT_EXPORT_MESSAGE, EsModuleDemo.DEFAULT_EXPORT_PARAMETER); + + javaScriptSupport.importEsModule("non-default-export") + .invoke("setMessage"); + + // Both .with() and .invoke() cause the function to be invoked + javaScriptSupport.importEsModule("parameterless-default-export") + .with(); + + javaScriptSupport.importEsModule("parameter-type-default-export") + .with(null, true, false, Math.PI * Math.E, "string", "jsonLiteral", + new JSONObject("key", "value"), new JSONArray(1, "2")); + + if (overrideEsModuleImportAgain != null && overrideEsModuleImportAgain) + { + javaScriptSupport.addEsModuleConfigurationCallback( + o -> EsModuleConfigurationCallback.setImport(o, + AppModule.OVERRIDDEN_ES_MODULE_ID, REQUEST_OVERRIDEN_MODULE_URL)); + } + + } + + void onActivate(boolean overrideEsModuleImportAgain) + { + this.overrideEsModuleImportAgain = overrideEsModuleImportAgain; + } + + Object onEnableOverride() + { + overrideEsModuleImportAgain = true; + return this; + } + + void onDisableOverride() + { + overrideEsModuleImportAgain = false; + } + + Object[] onPassivate() + { + return overrideEsModuleImportAgain != null && overrideEsModuleImportAgain + ? new Object[] { overrideEsModuleImportAgain } + : null; + } + +} diff --git a/tapestry-core/src/test/java/org/apache/tapestry5/integration/app1/pages/Index.java b/tapestry-core/src/test/java/org/apache/tapestry5/integration/app1/pages/Index.java index 025b7eb07..cbbc34c5d 100644 --- a/tapestry-core/src/test/java/org/apache/tapestry5/integration/app1/pages/Index.java +++ b/tapestry-core/src/test/java/org/apache/tapestry5/integration/app1/pages/Index.java @@ -630,7 +630,9 @@ public class Index new Item("RecursiveDemo","Recursive Demo","Recursive component example"), - new Item("SelfRecursiveDemo", "Self-Recursive Demo", "check for handling of self-recursive components") + new Item("SelfRecursiveDemo", "Self-Recursive Demo", "check for handling of self-recursive components"), + + new Item("EsModuleDemo", "ES Module Demo", "tests and demonstrations for the ES module support") ); static diff --git a/tapestry-core/src/test/java/org/apache/tapestry5/integration/app1/services/AppModule.java b/tapestry-core/src/test/java/org/apache/tapestry5/integration/app1/services/AppModule.java index 6801928b8..e8b91f4d0 100644 --- a/tapestry-core/src/test/java/org/apache/tapestry5/integration/app1/services/AppModule.java +++ b/tapestry-core/src/test/java/org/apache/tapestry5/integration/app1/services/AppModule.java @@ -50,6 +50,7 @@ import org.apache.tapestry5.ioc.annotations.Contribute; import org.apache.tapestry5.ioc.annotations.Marker; import org.apache.tapestry5.ioc.annotations.Value; import org.apache.tapestry5.ioc.services.ServiceOverride; +import org.apache.tapestry5.services.AssetSource; import org.apache.tapestry5.services.BeanBlockContribution; import org.apache.tapestry5.services.BeanBlockSource; import org.apache.tapestry5.services.ComponentClassResolver; @@ -58,6 +59,7 @@ import org.apache.tapestry5.services.LibraryMapping; import org.apache.tapestry5.services.ResourceDigestGenerator; import org.apache.tapestry5.services.ValueEncoderFactory; import org.apache.tapestry5.services.ValueLabelProvider; +import org.apache.tapestry5.services.javascript.EsModuleConfigurationCallback; import org.apache.tapestry5.services.pageload.PageCachingReferenceTypeService; import org.apache.tapestry5.services.pageload.PagePreloader; import org.apache.tapestry5.services.pageload.PreloaderMode; @@ -476,5 +478,43 @@ public class AppModule } } + + public static final String NON_OVERRIDDEN_ES_MODULE_ID = "nonOverriden"; + + public static final String NON_OVERRIDDEN_ES_MODULE_URL = "/nonOverridenURL"; + + public static final String OVERRIDDEN_ES_MODULE_ID = "overriden"; + + public static final String OVERRIDDEN_ES_MODULE_ORIGINAL_URL = "/originalURL"; + + public static final String OVERRIDDEN_ES_MODULE_NEW_URL = "/overridenURL"; + + public static void contributeEsModuleManager( + OrderedConfiguration<EsModuleConfigurationCallback> configuration, + AssetSource assetSource) + { + final String original = "OriginalCallback"; + final String override = "OverrideCallback"; + + configuration.add(override, + o -> EsModuleConfigurationCallback.setImport(o, OVERRIDDEN_ES_MODULE_ID, OVERRIDDEN_ES_MODULE_NEW_URL), + "after:" + original); + + configuration.add(original, + o -> { + EsModuleConfigurationCallback.setImport(o, NON_OVERRIDDEN_ES_MODULE_ID, NON_OVERRIDDEN_ES_MODULE_URL); + EsModuleConfigurationCallback.setImport(o, OVERRIDDEN_ES_MODULE_ID, OVERRIDDEN_ES_MODULE_ORIGINAL_URL); + }); + + configuration.add("Outside META-INF", o -> + EsModuleConfigurationCallback.setImport(o, "outside-metainf", + assetSource.getClasspathAsset("/org/apache/tapestry5/integration/app1/es-module-outside-metainf.js").toClientURL()) + ); + + configuration.add("External URL", o -> + EsModuleConfigurationCallback.setImport(o, "external/url", "https://example.com/module.js") + ); + + } } diff --git a/tapestry-core/src/test/java/org/apache/tapestry5/internal/services/javascript/EsModuleManagerImplTest.java b/tapestry-core/src/test/java/org/apache/tapestry5/internal/services/javascript/EsModuleManagerImplTest.java new file mode 100644 index 000000000..0e41bc12e --- /dev/null +++ b/tapestry-core/src/test/java/org/apache/tapestry5/internal/services/javascript/EsModuleManagerImplTest.java @@ -0,0 +1,93 @@ +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package org.apache.tapestry5.internal.services.javascript; + +import static org.testng.Assert.assertEquals; + +import org.apache.tapestry5.json.JSONArray; +import org.apache.tapestry5.json.JSONLiteral; +import org.apache.tapestry5.json.JSONObject; +import org.testng.annotations.Test; + +public class EsModuleManagerImplTest +{ + private static final String STRING = "asdfasdfasdfadsf"; + + private static final JSONLiteral JSON_LITERAL = new JSONLiteral("literally"); + private static final JSONArray JSON_ARRAY = new JSONArray("1", "true"); + private static JSONObject JSON_OBJECT = new JSONObject("something", "else", "array", + JSON_ARRAY, "literal", JSON_LITERAL); + private static Number NUMBER = Math.PI * Math.E; + + @Test + public void test_null_arguments() + { + assertEquals(convert(null, true), ""); + assertEquals(convert(null, false), ""); + } + + @Test + public void test_empty_arguments() + { + assertEquals(convert(new Object[0], true), ""); + assertEquals(convert(new Object[0], false), ""); + } + + @Test + public void test_one_argument() + { + assertEquals(convert(new Object[] {null}, false), null); + + assertEquals(convert(new Object[] {STRING}, false), quote(STRING)); + + assertEquals(convert(new Object[] {NUMBER}, false), NUMBER.toString()); + assertEquals(convert(new Object[] {Boolean.TRUE}, false), Boolean.TRUE.toString()); + assertEquals(convert(new Object[] {Boolean.FALSE}, false), Boolean.FALSE.toString()); + + assertEquals(convert(new Object[] {JSON_LITERAL}, false), quote(JSON_LITERAL.toString())); + + assertEquals(convert(new Object[] {JSON_ARRAY}, false), JSON_ARRAY.toString(false)); + assertEquals(convert(new Object[] {JSON_ARRAY}, true), JSON_ARRAY.toString(true)); + + assertEquals(convert(new Object[] {JSON_OBJECT}, false), JSON_OBJECT.toString(false)); + assertEquals(convert(new Object[] {JSON_OBJECT}, true), JSON_OBJECT.toString(true)); + + } + + @Test + public void test_multiple_arguments() + { + Object[] arguments = new Object[] { null, STRING, JSON_LITERAL, JSON_ARRAY, JSON_OBJECT }; + final String format = "null, '%s', '%s', %s, %s"; + + assertEquals(convert(arguments, false), + String.format(format, STRING, JSON_LITERAL, + JSON_ARRAY.toString(false), JSON_OBJECT.toString(false))); + + assertEquals(convert(arguments, true), + String.format(format, STRING, JSON_LITERAL, + JSON_ARRAY.toString(true), JSON_OBJECT.toString(true))); + + } + + private String quote(String string) + { + return "'" + string + "'"; + } + + private String convert(Object[] blah, boolean compactJSON) + { + return EsModuleManagerImpl.convertToJsFunctionParameters(blah, compactJSON); + } + +} diff --git a/tapestry-core/src/test/resources/META-INF/assets/es-modules/default-export.js b/tapestry-core/src/test/resources/META-INF/assets/es-modules/default-export.js new file mode 100644 index 000000000..671a2c21c --- /dev/null +++ b/tapestry-core/src/test/resources/META-INF/assets/es-modules/default-export.js @@ -0,0 +1,3 @@ +export default function(id, message) { + document.getElementById(id).innerHTML = message; +} \ No newline at end of file diff --git a/tapestry-core/src/test/resources/META-INF/assets/es-modules/foo/bar.js b/tapestry-core/src/test/resources/META-INF/assets/es-modules/foo/bar.js new file mode 100644 index 000000000..14a37c87b --- /dev/null +++ b/tapestry-core/src/test/resources/META-INF/assets/es-modules/foo/bar.js @@ -0,0 +1,2 @@ +console.log("Yup, I'm an ES module!"); +document.getElementById("message").innerHTML = "ES module foo/bar imported correctly!"; \ No newline at end of file diff --git a/tapestry-core/src/test/resources/META-INF/assets/es-modules/non-default-export.js b/tapestry-core/src/test/resources/META-INF/assets/es-modules/non-default-export.js new file mode 100644 index 000000000..f6cf336a0 --- /dev/null +++ b/tapestry-core/src/test/resources/META-INF/assets/es-modules/non-default-export.js @@ -0,0 +1,6 @@ +function setMessage() { + document.getElementById("non-default-export-message").innerHTML = + "Non-default exported function!"; +} + +export { setMessage }; \ No newline at end of file diff --git a/tapestry-core/src/test/resources/META-INF/assets/es-modules/parameter-type-default-export.js b/tapestry-core/src/test/resources/META-INF/assets/es-modules/parameter-type-default-export.js new file mode 100644 index 000000000..e4c3272e6 --- /dev/null +++ b/tapestry-core/src/test/resources/META-INF/assets/es-modules/parameter-type-default-export.js @@ -0,0 +1,14 @@ +export default function(nullValue, trueValue, falseValue, piTimesE, stringValue, jsonLiteralValue, + objectValue, arrayValue) { + + if (nullValue === null && (typeof trueValue === "boolean") && trueValue === true && + (typeof falseValue === "boolean") && falseValue === false && + (typeof piTimesE === "number") && piTimesE === Math.PI * Math.E && + (typeof jsonLiteralValue === "string") && jsonLiteralValue === "jsonLiteral" && + (typeof objectValue === "object") && objectValue.key === "value" && + arrayValue.constructor === Array && arrayValue[0] === 1 && arrayValue[1] === "2") { + + document.getElementById("parameter-type-default-export-message").innerHTML = "Parameter types passed correctly!"; + + } +} \ No newline at end of file diff --git a/tapestry-core/src/test/resources/META-INF/assets/es-modules/parameterless-default-export.js b/tapestry-core/src/test/resources/META-INF/assets/es-modules/parameterless-default-export.js new file mode 100644 index 000000000..32d1a2c0e --- /dev/null +++ b/tapestry-core/src/test/resources/META-INF/assets/es-modules/parameterless-default-export.js @@ -0,0 +1,3 @@ +export default function() { + document.getElementById("parameterless-default-export-message").innerHTML = "Parameterless default export!"; +} \ No newline at end of file diff --git a/tapestry-core/src/test/resources/META-INF/assets/es-modules/placement/body-bottom.js b/tapestry-core/src/test/resources/META-INF/assets/es-modules/placement/body-bottom.js new file mode 100644 index 000000000..2fbf023fd --- /dev/null +++ b/tapestry-core/src/test/resources/META-INF/assets/es-modules/placement/body-bottom.js @@ -0,0 +1,2 @@ +console.log("I should go into the bottom of <body>!"); +document.getElementById("body-bottom-message").innerHTML = "ES module imported correctly (<body> bottom)!"; \ No newline at end of file diff --git a/tapestry-core/src/test/resources/META-INF/assets/es-modules/placement/body-top.js b/tapestry-core/src/test/resources/META-INF/assets/es-modules/placement/body-top.js new file mode 100644 index 000000000..4b9e268c3 --- /dev/null +++ b/tapestry-core/src/test/resources/META-INF/assets/es-modules/placement/body-top.js @@ -0,0 +1,2 @@ +console.log("I should go into the top of <body>!"); +document.getElementById("body-top-message").innerHTML = "ES module imported correctly (<body> top)!"; \ No newline at end of file diff --git a/tapestry-core/src/test/resources/META-INF/assets/es-modules/placement/head.js b/tapestry-core/src/test/resources/META-INF/assets/es-modules/placement/head.js new file mode 100644 index 000000000..ac86164ef --- /dev/null +++ b/tapestry-core/src/test/resources/META-INF/assets/es-modules/placement/head.js @@ -0,0 +1,2 @@ +console.log("I should go into <head>!"); +document.getElementById("head-message").innerHTML = "ES module imported correctly (<head>)!"; \ No newline at end of file diff --git a/tapestry-core/src/test/resources/META-INF/assets/es-modules/root-folder.js b/tapestry-core/src/test/resources/META-INF/assets/es-modules/root-folder.js new file mode 100644 index 000000000..db38f9256 --- /dev/null +++ b/tapestry-core/src/test/resources/META-INF/assets/es-modules/root-folder.js @@ -0,0 +1,2 @@ +console.log("I'm in the root folder!"); +document.getElementById("root-folder-message").innerHTML = "ES module imported correctly from the root folder!"; \ No newline at end of file diff --git a/tapestry-core/src/test/resources/META-INF/assets/es-modules/show-import-map.js b/tapestry-core/src/test/resources/META-INF/assets/es-modules/show-import-map.js new file mode 100644 index 000000000..7bfd34b48 --- /dev/null +++ b/tapestry-core/src/test/resources/META-INF/assets/es-modules/show-import-map.js @@ -0,0 +1,2 @@ +let importMap = document.querySelector("script[type=importmap]").innerHTML; +document.getElementById("import-map-listing").innerHTML = importMap; \ No newline at end of file diff --git a/tapestry-core/src/test/resources/org/apache/tapestry5/integration/app1/es-module-outside-metainf.js b/tapestry-core/src/test/resources/org/apache/tapestry5/integration/app1/es-module-outside-metainf.js new file mode 100644 index 000000000..f41c552ca --- /dev/null +++ b/tapestry-core/src/test/resources/org/apache/tapestry5/integration/app1/es-module-outside-metainf.js @@ -0,0 +1,2 @@ +console.log("ES module outside /META-INF/assets/es-modules/"); +document.getElementById("outside-metainf-message").innerHTML = "ES module correctly imported from outside /META-INF/assets/es-modules!"; \ No newline at end of file diff --git a/tapestry-core/src/test/resources/org/apache/tapestry5/integration/app1/pages/EsModuleDemo.tml b/tapestry-core/src/test/resources/org/apache/tapestry5/integration/app1/pages/EsModuleDemo.tml new file mode 100644 index 000000000..702834fb8 --- /dev/null +++ b/tapestry-core/src/test/resources/org/apache/tapestry5/integration/app1/pages/EsModuleDemo.tml @@ -0,0 +1,34 @@ +<html t:type="Border" xmlns:t="http://tapestry.apache.org/schema/tapestry_5_3.xsd" xmlns:p="tapestry:parameter"> + + <h1>ES Module Import Demo</h1> + + <p> + <t:if test="overrideEsModuleImportAgain"> + <p:then> + <a t:type="EventLink" event="disableOverride" class="switch">Don't override ES module import again.</a> + </p:then> + <p:else> + <a t:type="EventLink" event="enableOverride" class="switch">Override ES module import again.</a> + </p:else> + </t:if> + </p> + + <p id="message"/> + <p id="head-message"/> + <p id="body-top-message"/> + <p id="body-bottom-message"/> + <p id="root-folder-message"/> + <p id="outside-metainf-message"/> + <p id="${DEFAULT_EXPORT_MESSAGE}"/> + <p id="non-default-export-message"/> + <p id="parameterless-default-export-message"/> + <p id="parameter-type-default-export-message"/> + + <p> + Import map: + </p> + <pre id="import-map-listing"/> + + <p id="last-body-element"></p> + +</html> \ No newline at end of file diff --git a/tapestry-ioc/src/main/java/org/apache/tapestry5/ioc/internal/util/URLChangeTracker.java b/tapestry-ioc/src/main/java/org/apache/tapestry5/ioc/internal/util/URLChangeTracker.java index 4baafe32c..af365db75 100644 --- a/tapestry-ioc/src/main/java/org/apache/tapestry5/ioc/internal/util/URLChangeTracker.java +++ b/tapestry-ioc/src/main/java/org/apache/tapestry5/ioc/internal/util/URLChangeTracker.java @@ -18,7 +18,10 @@ import java.io.File; import java.io.IOException; import java.net.URISyntaxException; import java.net.URL; +import java.util.ArrayList; +import java.util.Collections; import java.util.HashSet; +import java.util.List; import java.util.Map; import java.util.Set; @@ -314,6 +317,24 @@ public class URLChangeTracker<T> return fileToTimestamp.size(); } + public String toString() + { + StringBuilder builder = new StringBuilder(); + + final List<File> files = new ArrayList<>(fileToTimestamp.keySet()); + Collections.sort(files, (f1, f2) -> f1.getName().compareTo(f2.getName())); + + for (File file : files) + { + builder.append(file.getName()); + builder.append(": "); + builder.append(fileToTimestamp.get(file)); + builder.append("\n"); + } + + return builder.toString(); + } + private final class TrackingInfo {