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}