001/* 002 * Shredzone Commons 003 * 004 * Copyright (C) 2012 Richard "Shred" Körber 005 * http://commons.shredzone.org 006 * 007 * This program is free software: you can redistribute it and/or modify 008 * it under the terms of the GNU Library General Public License as 009 * published by the Free Software Foundation, either version 3 of the 010 * License, or (at your option) any later version. 011 * 012 * This program is distributed in the hope that it will be useful, 013 * but WITHOUT ANY WARRANTY; without even the implied warranty of 014 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 015 * GNU General Public License for more details. 016 * 017 * You should have received a copy of the GNU Library General Public License 018 * along with this program. If not, see <http://www.gnu.org/licenses/>. 019 */ 020 021package org.shredzone.commons.view.manager; 022 023import java.util.ArrayList; 024import java.util.Collections; 025import java.util.HashMap; 026import java.util.List; 027import java.util.Map; 028import java.util.regex.Matcher; 029import java.util.regex.Pattern; 030import java.util.stream.IntStream; 031 032import javax.annotation.Nonnull; 033import javax.annotation.ParametersAreNonnullByDefault; 034import javax.annotation.concurrent.Immutable; 035 036import org.shredzone.commons.view.PathContext; 037import org.shredzone.commons.view.Signature; 038import org.shredzone.commons.view.annotation.View; 039import org.shredzone.commons.view.util.PathUtils; 040import org.springframework.expression.EvaluationContext; 041import org.springframework.expression.Expression; 042import org.springframework.expression.ExpressionParser; 043import org.springframework.expression.spel.standard.SpelExpressionParser; 044 045/** 046 * A view pattern is a URL pattern for a view. It is used to detect which view is used for 047 * handling a HTTP request, and what parameters are used. 048 * <p> 049 * {@link ViewPattern ViewPatterns} are immutable. 050 * 051 * @author Richard "Shred" Körber 052 */ 053@ParametersAreNonnullByDefault 054@Immutable 055public class ViewPattern implements Comparable<ViewPattern> { 056 private static final Pattern PATH_PART = Pattern.compile("\\$\\{([^\\}]+)\\}"); 057 058 private final String pattern; 059 private final ViewInvoker invoker; 060 private final Signature signature; 061 private final Pattern regEx; 062 private final List<Expression> expression; 063 private final List<String> parameter; 064 private final int weight; 065 private final String qualifier; 066 067 /** 068 * Instantiates a new view pattern. 069 * 070 * @param anno 071 * {@link View} annotation 072 * @param invoker 073 * {@link ViewInvoker} for rendering this view 074 */ 075 public ViewPattern(View anno, ViewInvoker invoker) { 076 this.invoker = invoker; 077 this.pattern = anno.pattern(); 078 079 if (anno.qualifier() != null && !anno.qualifier().isEmpty()) { 080 this.qualifier = anno.qualifier(); 081 } else { 082 this.qualifier = null; 083 } 084 085 String[] sig = anno.signature(); 086 if (sig != null && sig.length > 0) { 087 this.signature = new Signature(sig); 088 } else { 089 this.signature = null; 090 } 091 092 List<Expression> expList = new ArrayList<>(); 093 List<String> paramList = new ArrayList<>(); 094 StringBuilder pb = new StringBuilder(); 095 compilePattern(this.pattern, pb, expList, paramList); 096 this.regEx = Pattern.compile(pb.toString()); 097 this.expression = Collections.unmodifiableList(expList); 098 this.parameter = Collections.unmodifiableList(paramList); 099 100 this.weight = computeWeight(this.pattern); 101 } 102 103 /** 104 * Gets the signature stored in this {@link ViewPattern}. 105 * 106 * @return {@link Signature} 107 */ 108 public Signature getSignature() { 109 return signature; 110 } 111 112 /** 113 * Gets the {@link ViewInvoker} to be used for rendering. 114 * 115 * @return {@link ViewInvoker} 116 */ 117 public ViewInvoker getInvoker() { 118 return invoker; 119 } 120 121 /** 122 * Gets the weight of this {@link ViewPattern}. If more than one {@link ViewPattern} 123 * matches the requested URL, the one with the highest weight is taken. 124 * 125 * @return weight 126 */ 127 public int getWeight() { 128 return weight; 129 } 130 131 /** 132 * Gets the view's URL pattern. 133 * 134 * @return URL pattern 135 */ 136 public String getPattern() { 137 return pattern; 138 } 139 140 /** 141 * Gets a regular expression {@link Pattern} to match a URL against this 142 * {@link ViewPattern}. This regular expression can be used to quickly find view 143 * candidates for a request URL. 144 * 145 * @return regular expression {@link Pattern}. 146 */ 147 public @Nonnull Pattern getRegEx() { 148 return regEx; 149 } 150 151 /** 152 * Returns an {@link Expression} for each placeholder in the pattern. The expressions 153 * are used for building an URL to this view. 154 * 155 * @return List of {@link Expression} 156 */ 157 public @Nonnull List<Expression> getExpression() { 158 return expression; 159 } 160 161 /** 162 * Returns a list of parameter strings for each placeholder in the pattern. 163 * 164 * @return List of parameters 165 */ 166 public @Nonnull List<String> getParameters() { 167 return parameter; 168 } 169 170 /** 171 * Returns the qualifier of this pattern. 172 * 173 * @return Qualifier of this pattern, or {@code null} for the default qualifier. 174 */ 175 public String getQualifier() { 176 return qualifier; 177 } 178 179 /** 180 * Matches the requested URL against this {@link ViewPattern}. 181 * 182 * @param path 183 * the requested URL 184 * @return {@code true} if this {@link ViewPattern} matches the given URL, and thus is 185 * a candidate for rendering 186 */ 187 public boolean matches(String path) { 188 return regEx.matcher(path).matches(); 189 } 190 191 /** 192 * Resolves a requested URL path. For each placeholder in the view pattern, the 193 * placeholder name and its value in the URL path is returned in a map. 194 * 195 * @param path 196 * the requested URL to be resolved 197 * @return Map containing the placeholder names and its values 198 */ 199 public Map<String, String> resolve(String path) { 200 Matcher m = regEx.matcher(path); 201 if (!m.matches()) { 202 return null; 203 } 204 205 if (m.groupCount() != parameter.size()) { 206 throw new IllegalStateException("regex group count " + m.groupCount() 207 + " does not match parameter count " + parameter.size()); 208 } 209 210 // TODO: only use decode when #encode() was used 211 return IntStream.range(0, parameter.size()).collect( 212 HashMap::new, 213 (map, ix) -> map.put(parameter.get(ix), PathUtils.decode(m.group(ix + 1))), 214 Map::putAll 215 ); 216 } 217 218 /** 219 * Evaluates the given {@link EvaluationContext} and builds an URL to the appropriate 220 * view. 221 * 222 * @param context 223 * {@link EvaluationContext} to be used 224 * @param data 225 * {@link PathContext} containing all data required for building the URL 226 * @return URL that was built, or {@code null} if the {@link PathContext} did not 227 * contain all necessary data for building the URL 228 */ 229 public String evaluate(EvaluationContext context, PathContext data) { 230 StringBuilder sb = new StringBuilder(); 231 for (Expression expr : expression) { 232 String value = expr.getValue(context, data, String.class); 233 if (value == null) { 234 // A part resolved to null, so this ViewPattern is unable 235 // to build a path from the given PathData. 236 return null; 237 } 238 sb.append(value); 239 } 240 241 // Remove ugly double slashes 242 int pos; 243 while ((pos = sb.indexOf("//")) >= 0) { 244 sb.deleteCharAt(pos); 245 } 246 247 return sb.toString(); 248 } 249 250 /** 251 * Compiles a view pattern. Generates a parameter list, a list of expressions for 252 * building URLs to this view, and a regular expression for matching URLs against this 253 * view pattern. 254 * 255 * @param pstr 256 * the view pattern 257 * @param pattern 258 * {@link StringBuilder} to assemble the regular expression in 259 * @param expList 260 * List of {@link Expression} to assemble expressions in 261 * @param paramList 262 * List to assemble parameters in 263 */ 264 private void compilePattern(String pstr, StringBuilder pattern, 265 List<Expression> expList, List<String> paramList) { 266 ExpressionParser parser = new SpelExpressionParser(); 267 int previous = 0; 268 269 Matcher m = PATH_PART.matcher(pstr); 270 while (m.find()) { 271 String fixedPart = pstr.substring(previous, m.start()); 272 if (fixedPart.indexOf('\'') >= 0) { 273 throw new IllegalArgumentException("path parameters must not contain \"'\""); 274 } 275 276 String expressionPart = m.group(1); 277 278 pattern.append(Pattern.quote(fixedPart)); 279 pattern.append("([^/]*)"); 280 281 paramList.add(expressionPart); 282 283 expList.add(parser.parseExpression('\'' + fixedPart + '\'')); 284 expList.add(parser.parseExpression(expressionPart)); 285 286 previous = m.end(); 287 } 288 289 String postPart = pstr.substring(previous); 290 pattern.append(Pattern.quote(postPart)); 291 expList.add(parser.parseExpression('\'' + postPart + '\'')); 292 } 293 294 /** 295 * Computes the weight of the pattern. The weight is computed by a score where every 296 * path delimiter '/' counts 10, constant character counts 5 and every path parameter 297 * counts 1. 298 * 299 * @param pstr 300 * view pattern to weight 301 * @return weight of this pattern 302 */ 303 private int computeWeight(String pstr) { 304 int count = 0; 305 int pos = 0; 306 while (pos < pstr.length()) { 307 char ch = pstr.charAt(pos); 308 if (ch == '/') { 309 count += 10; 310 311 } else if (ch == '$' && pos + 1 < pstr.length() && pstr.charAt(pos + 1) == '{') { 312 int end = pstr.indexOf('}', pos); 313 if (end >= 0) { 314 pos = end; 315 count += 1; 316 } 317 318 } else { 319 count += 5; 320 } 321 322 pos++; 323 } 324 return count; 325 } 326 327 @Override 328 public int compareTo(ViewPattern o) { 329 return o.getWeight() - getWeight(); 330 } 331 332 @Override 333 public boolean equals(Object obj) { 334 if (obj == null || (!(obj instanceof ViewPattern))) { 335 return false; 336 } 337 338 return compareTo((ViewPattern) obj) == 0; 339 } 340 341 @Override 342 public int hashCode() { 343 return Integer.hashCode(getWeight()); 344 } 345 346}