001package ball.spring.dialect; 002/*- 003 * ########################################################################## 004 * Reusable Spring Components 005 * $Id: WebJarsDialect.java 7215 2021-01-03 18:39:51Z ball $ 006 * $HeadURL: svn+ssh://svn.hcf.dev/var/spool/scm/repository.svn/ball-spring/trunk/jar/src/main/java/ball/spring/dialect/WebJarsDialect.java $ 007 * %% 008 * Copyright (C) 2018 - 2021 Allen D. Ball 009 * %% 010 * Licensed under the Apache License, Version 2.0 (the "License"); 011 * you may not use this file except in compliance with the License. 012 * You may obtain a copy of the License at 013 * 014 * http://www.apache.org/licenses/LICENSE-2.0 015 * 016 * Unless required by applicable law or agreed to in writing, software 017 * distributed under the License is distributed on an "AS IS" BASIS, 018 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 019 * See the License for the specific language governing permissions and 020 * limitations under the License. 021 * ########################################################################## 022 */ 023import ball.annotation.CompileTimeCheck; 024import java.util.Collection; 025import java.util.Collections; 026import java.util.LinkedHashSet; 027import java.util.Map; 028import java.util.Set; 029import java.util.regex.Matcher; 030import java.util.regex.Pattern; 031import java.util.stream.Stream; 032import lombok.Getter; 033import lombok.NoArgsConstructor; 034import lombok.ToString; 035import lombok.extern.log4j.Log4j2; 036import org.springframework.util.AntPathMatcher; 037import org.thymeleaf.IEngineConfiguration; 038import org.thymeleaf.context.IExpressionContext; 039import org.thymeleaf.context.ITemplateContext; 040import org.thymeleaf.dialect.AbstractProcessorDialect; 041import org.thymeleaf.dialect.IExpressionObjectDialect; 042import org.thymeleaf.engine.AttributeName; 043import org.thymeleaf.expression.IExpressionObjectFactory; 044import org.thymeleaf.model.IProcessableElementTag; 045import org.thymeleaf.processor.IProcessor; 046import org.thymeleaf.processor.element.AbstractAttributeTagProcessor; 047import org.thymeleaf.processor.element.IElementTagStructureHandler; 048import org.thymeleaf.standard.expression.IStandardExpression; 049import org.thymeleaf.standard.expression.IStandardExpressionParser; 050import org.thymeleaf.standard.expression.StandardExpressions; 051import org.webjars.WebJarAssetLocator; 052 053import static java.util.stream.Collectors.toCollection; 054import static java.util.stream.Collectors.toSet; 055import static org.thymeleaf.templatemode.TemplateMode.HTML; 056 057/** 058 * {@link WebJarAssetLocator} Thymeleaf dialect. 059 * 060 * @author {@link.uri mailto:ball@hcf.dev Allen D. Ball} 061 * @version $Revision: 7215 $ 062 */ 063@ToString @Log4j2 064public class WebJarsDialect extends AbstractProcessorDialect 065 implements IExpressionObjectDialect { 066 private static final String NAME = "WebJars Dialect"; 067 private static final String PREFIX = "webjars"; 068 private static final int PRECEDENCE = 9999; 069 070 @CompileTimeCheck 071 private static final Pattern PATTERN = 072 Pattern.compile("(?is)" 073 + "(?<prefix>.*" + WebJarAssetLocator.WEBJARS_PATH_PREFIX + ")" 074 + "(?<path>/.*)"); 075 076 private static final String LOCAL_FORMAT = "/webjars%s"; 077 private static final String CDN_FORMAT = 078 "%s://cdn.jsdelivr.net/webjars/%s%s"; 079 080 @Getter(lazy = true) 081 private final IExpressionObjectFactory expressionObjectFactory = 082 new ExpressionObjectFactory(); 083 084 /** 085 * Sole constructor. 086 */ 087 public WebJarsDialect() { super(NAME, PREFIX, PRECEDENCE); } 088 089 @Override 090 public Set<IProcessor> getProcessors(String prefix) { 091 Set<IProcessor> set = 092 Stream.of("href", "src") 093 .map(t -> new PathAttributeTagProcessor(prefix, t)) 094 .collect(toSet()); 095 096 return set; 097 } 098 099 private static String path(WebJarAssetLocator locator, 100 boolean useCdn, String scheme, String path) { 101 String resource = locator.getFullPath(path); 102 Matcher matcher = PATTERN.matcher(resource); 103 104 if (matcher.matches()) { 105 /* 106 * FIX: groupId will always be null in a shaded JAR. 107 */ 108 String groupId = locator.groupId(resource); 109 110 if (useCdn && groupId != null) { 111 path = 112 String.format(CDN_FORMAT, 113 (scheme != null) ? scheme : "http", 114 groupId, matcher.group("path")); 115 } else { 116 path = 117 String.format(LOCAL_FORMAT, 118 matcher.group("path")); 119 } 120 } 121 122 return path; 123 } 124 125 @ToString 126 private static class PathAttributeTagProcessor extends AbstractAttributeTagProcessor { 127 private final WebJarAssetLocator locator = new WebJarAssetLocator(); 128 129 public PathAttributeTagProcessor(String prefix, String name) { 130 super(HTML, prefix, null, false, name, true, PRECEDENCE, true); 131 } 132 133 @Override 134 protected void doProcess(ITemplateContext context, 135 IProcessableElementTag tag, 136 AttributeName name, String value, 137 IElementTagStructureHandler handler) { 138 IEngineConfiguration configuration = context.getConfiguration(); 139 IStandardExpressionParser parser = 140 StandardExpressions.getExpressionParser(configuration); 141 IStandardExpression expression = 142 parser.parseExpression(context, value); 143 String path = (String) expression.execute(context); 144 String scheme = 145 (String) 146 parser.parseExpression(context, "${#request.scheme}") 147 .execute(context); 148 149 try { 150 path = path(locator, false, scheme, path); 151 } catch (IllegalArgumentException exception) { 152 } 153 154 handler.setAttribute(name.getAttributeName(), path); 155 } 156 } 157 158 @NoArgsConstructor @ToString 159 private static class ExpressionObjectFactory implements IExpressionObjectFactory { 160 private final Map<String,Object> map = 161 Collections.singletonMap(PREFIX, new WebJars()); 162 163 @Override 164 public Set<String> getAllExpressionObjectNames() { 165 return map.keySet(); 166 } 167 168 @Override 169 public Object buildObject(IExpressionContext context, String name) { 170 return (name != null) ? map.get(name) : null; 171 } 172 173 @Override 174 public boolean isCacheable(String name) { 175 return name != null && map.containsKey(name); 176 } 177 } 178 179 /** 180 * {@link WebJars} Thymeleaf dialect expression object implementation. 181 */ 182 @NoArgsConstructor @ToString 183 public static class WebJars { 184 @CompileTimeCheck 185 private static final Pattern PATTERN = 186 Pattern.compile("(?i)[\\p{Space},]+"); 187 188 private final WebJarAssetLocator locator = new WebJarAssetLocator(); 189 private final Set<String> assets = locator.listAssets(); 190 private final AntPathMatcher matcher = new AntPathMatcher(); 191 192 /** 193 * Method to return WebJar resources matching {@link AntPathMatcher} 194 * patterns. 195 * 196 * @param patterns The {@link AntPathMatcher} patterns to 197 * match. 198 * 199 * @return The matching resource paths. 200 */ 201 public Collection<String> assets(String... patterns) { 202 return assets(false, null, patterns); 203 } 204 205 /** 206 * Method to return WebJar resources matching {@link AntPathMatcher} 207 * patterns. 208 * 209 * @param useCdn {@code true} to provide a CDN 210 * {@code URI}; {@code false} for a local 211 * path. 212 * @param scheme The {@code URI} scheme. 213 * @param patterns The {@link AntPathMatcher} patterns to 214 * match. 215 * 216 * @return The matching resource paths. 217 */ 218 public Collection<String> assets(boolean useCdn, String scheme, 219 String... patterns) { 220 Collection<String> collection = 221 Stream.of(patterns) 222 .flatMap(t -> PATTERN.splitAsStream(t)) 223 .flatMap(t -> assets.stream().filter(a -> matcher.match(t, a))) 224 .map(t -> path(locator, useCdn, scheme, t)) 225 .collect(toCollection(LinkedHashSet::new)); 226 227 return collection; 228 } 229 230 /** 231 * Method to convert a WebJar resource (partial) path to its 232 * corresponding CDN (URI) path. Typical Thymeleaf usage: 233 * 234 * {@code @{${#webjars.cdn(#request.scheme, path)}}}. 235 * 236 * @param scheme The {@code URI} scheme. 237 * @param path The (possibly partial) path. 238 * 239 * @return The CDN URI if one may be constructed; {@code path} 240 * otherwise. 241 */ 242 public String cdn(String scheme, String path) { 243 try { 244 path = path(locator, false, null, path); 245 } catch (IllegalArgumentException exception) { 246 } 247 248 return path; 249 } 250 251 /** 252 * See {@link WebJarAssetLocator#getWebJars()}. 253 * 254 * @return Result of {@link WebJarAssetLocator#getWebJars()} call. 255 */ 256 public Map<String,String> getJars() { return locator.getWebJars(); } 257 } 258}