diff --git a/wicket-core-tests/src/test/java/org/apache/wicket/core/util/string/JsonUtilsTest.java b/wicket-core-tests/src/test/java/org/apache/wicket/core/util/string/JsonUtilsTest.java new file mode 100644 index 00000000000..dee403efec4 --- /dev/null +++ b/wicket-core-tests/src/test/java/org/apache/wicket/core/util/string/JsonUtilsTest.java @@ -0,0 +1,40 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.wicket.core.util.string; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.apache.wicket.response.StringResponse; +import org.apache.wicket.util.value.AttributeMap; +import org.junit.jupiter.api.Test; + +class JsonUtilsTest +{ + @Test + public void writeInlineScript() + { + StringResponse response = new StringResponse(); + AttributeMap attributes = new AttributeMap(); + attributes.putAttribute(JavaScriptUtils.ATTR_TYPE, "importmap"); + JsonUtils.writeInlineScript(response, + "{\"imports\":{\"\n"); + } +} diff --git a/wicket-core/src/main/java/org/apache/wicket/markup/head/JavaScriptContentHeaderItem.java b/wicket-core/src/main/java/org/apache/wicket/markup/head/JavaScriptContentHeaderItem.java index 0c8839d6f47..85ce9a1a255 100644 --- a/wicket-core/src/main/java/org/apache/wicket/markup/head/JavaScriptContentHeaderItem.java +++ b/wicket-core/src/main/java/org/apache/wicket/markup/head/JavaScriptContentHeaderItem.java @@ -34,6 +34,8 @@ public class JavaScriptContentHeaderItem extends JavaScriptHeaderItem { private final CharSequence javaScript; + private JavaScriptReferenceType type = JavaScriptReferenceType.TEXT_JAVASCRIPT; + /** * Creates a new {@code JavaScriptContentHeaderItem}. * @@ -57,11 +59,28 @@ public CharSequence getJavaScript() return javaScript; } + public JavaScriptReferenceType getType() { + return type; + } + + /** + * Set the type of + * the script. If no type is set, it defaults to {@link JavaScriptReferenceType#TEXT_JAVASCRIPT}. + * + * @param type the new type. + */ + public JavaScriptContentHeaderItem setType(final JavaScriptReferenceType type) { + this.type = type; + return this; + } + @Override public void render(Response response) { AttributeMap attributes = new AttributeMap(); - attributes.putAttribute(JavaScriptUtils.ATTR_TYPE, "text/javascript"); + // An empty string works the same as `text/javascript`, but use the latter for backward compatibility. + JavaScriptReferenceType actualType = type == null ? JavaScriptReferenceType.TEXT_JAVASCRIPT : type; + attributes.putAttribute(JavaScriptUtils.ATTR_TYPE, actualType.getType()); attributes.putAttribute(JavaScriptUtils.ATTR_ID, getId()); attributes.putAttribute(JavaScriptUtils.ATTR_CSP_NONCE, getNonce()); JavaScriptUtils.writeInlineScript(response, getJavaScript(), attributes); @@ -88,7 +107,8 @@ public boolean equals(Object o) if (o == null || getClass() != o.getClass()) return false; if (!super.equals(o)) return false; JavaScriptContentHeaderItem that = (JavaScriptContentHeaderItem) o; - return Objects.equals(javaScript, that.javaScript); + return Objects.equals(javaScript, that.javaScript) && + Objects.equals(type, that.type); } @Override @@ -97,6 +117,7 @@ public int hashCode() // Not using `Objects.hash` for performance reasons int result = super.hashCode(); result = 31 * result + ((javaScript != null) ? javaScript.hashCode() : 0); + result = 31 * result + ((type != null) ? type.hashCode() : 0); return result; } } diff --git a/wicket-core/src/main/java/org/apache/wicket/markup/head/JavaScriptImportMapHeaderItem.java b/wicket-core/src/main/java/org/apache/wicket/markup/head/JavaScriptImportMapHeaderItem.java new file mode 100644 index 00000000000..a61f7c5c2fe --- /dev/null +++ b/wicket-core/src/main/java/org/apache/wicket/markup/head/JavaScriptImportMapHeaderItem.java @@ -0,0 +1,123 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.wicket.markup.head; + +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.Map; + +import com.github.openjson.JSONObject; +import com.github.openjson.JSONStringer; +import org.apache.wicket.core.util.string.JavaScriptUtils; +import org.apache.wicket.core.util.string.JsonUtils; +import org.apache.wicket.request.Response; +import org.apache.wicket.request.cycle.RequestCycle; +import org.apache.wicket.request.resource.JavaScriptResourceReference; +import org.apache.wicket.util.LazyInitializer; +import org.apache.wicket.util.string.Strings; +import org.apache.wicket.util.value.AttributeMap; + +/** + * {@link HeaderItem} for + * import + * maps. + */ +public class JavaScriptImportMapHeaderItem extends JavaScriptHeaderItem +{ + private final Map resourceReferencesByModuleSpecifier; + private final Map> resourceReferencesByModuleSpecifierByScopeUrl; + private final Map> integrityHashesByResourceReference; + private final LazyInitializer lazyJson = new LazyInitializer<>() + { + @Override + protected String createInstance() + { + JSONObject importMap = new JSONObject(); + + if (resourceReferencesByModuleSpecifier != null) { + importMap.put("imports", createImportsObject(resourceReferencesByModuleSpecifier)); + } + + if (resourceReferencesByModuleSpecifierByScopeUrl != null) { + JSONObject scopes = new JSONObject(); + resourceReferencesByModuleSpecifierByScopeUrl + .forEach((scopeUrl, resourceReferencesByModuleSpecifier) -> + scopes.put(scopeUrl, createImportsObject(resourceReferencesByModuleSpecifier))); + importMap.put("scopes", scopes); + } + + if (integrityHashesByResourceReference != null) { + JSONObject integrity = new JSONObject(); + integrityHashesByResourceReference.forEach((resourceReference, hashes) -> + integrity.put(RequestCycle.get().urlFor(resourceReference, null).toString(), String.join(" ", hashes))); + importMap.put("integrity", integrity); + } + + return importMap.toString(new JSONStringer()); + } + + private static JSONObject createImportsObject( + Map resourceReferencesByModuleSpecifier + ) + { + JSONObject imports = new JSONObject(); + resourceReferencesByModuleSpecifier + .forEach((moduleSpecifier, resourceReference) -> + imports.put(moduleSpecifier, RequestCycle.get().urlFor(resourceReference, null))); + return imports; + } + }; + + /** + * Create a header item for an import map. All fields are optional. + * + * @param resourceReferencesByModuleSpecifier the base module specifier map. + * @param resourceReferencesByModuleSpecifierByScopeUrl module specifier maps for scripts with specific URLs that + * import modules. + * @param integrityHashesByResourceReference lists of hashes of the resources. + */ + public JavaScriptImportMapHeaderItem( + final Map resourceReferencesByModuleSpecifier, + final Map> resourceReferencesByModuleSpecifierByScopeUrl, + final Map> integrityHashesByResourceReference + ) + { + this.resourceReferencesByModuleSpecifier = resourceReferencesByModuleSpecifier; + this.resourceReferencesByModuleSpecifierByScopeUrl = resourceReferencesByModuleSpecifierByScopeUrl; + this.integrityHashesByResourceReference = integrityHashesByResourceReference; + } + + @Override + public Iterable getRenderTokens() + { + String json = lazyJson.get(); + if (Strings.isEmpty(getId())) + return Collections.singletonList(json); + return Arrays.asList(getId(), json); + } + + @Override + public void render(Response response) + { + AttributeMap attributes = new AttributeMap(); + attributes.putAttribute(JavaScriptUtils.ATTR_TYPE, "importmap"); + attributes.putAttribute(JavaScriptUtils.ATTR_ID, getId()); + attributes.putAttribute(JavaScriptUtils.ATTR_CSP_NONCE, getNonce()); + JsonUtils.writeInlineScript(response, lazyJson.get(), attributes); + } +} diff --git a/wicket-core/src/main/java/org/apache/wicket/markup/head/JavaScriptReferenceType.java b/wicket-core/src/main/java/org/apache/wicket/markup/head/JavaScriptReferenceType.java index e14ab6f5c4f..a496f6745e6 100644 --- a/wicket-core/src/main/java/org/apache/wicket/markup/head/JavaScriptReferenceType.java +++ b/wicket-core/src/main/java/org/apache/wicket/markup/head/JavaScriptReferenceType.java @@ -21,7 +21,11 @@ /** * To be used to define the "type" attribute of the script tag written - * by a {@link AbstractJavaScriptReferenceHeaderItem}. + * by a {@link AbstractJavaScriptReferenceHeaderItem} or a + * {@link JavaScriptContentHeaderItem}. + *

+ * This class should be called JavaScriptType, but + * renaming breaks backward compatibility. */ public class JavaScriptReferenceType implements IClusterable {