blob: d170c03c9344e5697efb09fbbb09db26163046d4 [file] [log] [blame]
/*
* 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 com.opensymphony.xwork2.config.impl;
import com.opensymphony.xwork2.util.PatternMatcher;
import org.apache.commons.lang3.math.NumberUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import java.io.Serializable;
import java.util.*;
/**
* <p> Matches patterns against pre-compiled wildcard expressions pulled from
* target objects. It uses the wildcard matcher from the Apache Cocoon
* project. Patterns will be matched in the order they were added. The first
* match wins, so more specific patterns should be defined before less specific
* patterns.
*
* @since 2.1
*/
public abstract class AbstractMatcher<E> implements Serializable {
private static final Logger LOG = LogManager.getLogger(AbstractMatcher.class);
/**
* <p> The logging instance </p>
*/
private static final Logger log = LogManager.getLogger(AbstractMatcher.class);
/**
* <p> Handles all wildcard pattern matching. </p>
*/
PatternMatcher<Object> wildcard;
/**
* <p> The compiled patterns and their associated target objects </p>
*/
List<Mapping<E>> compiledPatterns = new ArrayList<>();
/**
* This flag controls if passed named params should be appended
* to the map in {@link #replaceParameters(Map, Map)}
* and will be accessible in {@link com.opensymphony.xwork2.config.entities.ResultConfig}.
* If set to false, the named parameters won't be appended.
*
* This behaviour is controlled by {@link org.apache.struts2.StrutsConstants#STRUTS_MATCHER_APPEND_NAMED_PARAMETERS}
*
* @since 2.5.23
* See WW-5065
*/
private final boolean appendNamedParameters;
public AbstractMatcher(PatternMatcher<?> helper, boolean appendNamedParameters) {
this.wildcard = (PatternMatcher<Object>) helper;
this.appendNamedParameters = appendNamedParameters;
}
/**
* <p>
* Finds and precompiles the wildcard patterns. Patterns will be evaluated
* in the order they were added. Only patterns that actually contain a
* wildcard will be compiled.
* </p>
*
* <p>
* Patterns can optionally be matched "loosely". When the end of the pattern
* matches \*[^*]\*$ (wildcard, no wildcard, wildcard), if the pattern
* fails, it is also matched as if the last two characters didn't exist. The
* goal is to support the legacy "*!*" syntax, where the "!*" is optional.
* </p>
*
* @param name The pattern
* @param target The object to associate with the pattern
* @param looseMatch
* To loosely match wildcards or not
*/
public void addPattern(String name, E target, boolean looseMatch) {
Object pattern;
if (!wildcard.isLiteral(name)) {
if (looseMatch && (name.length() > 0) && (name.charAt(0) == '/')) {
name = name.substring(1);
}
log.debug("Compiling pattern '{}'", name);
pattern = wildcard.compilePattern(name);
compiledPatterns.add(new Mapping<E>(name, pattern, target));
if (looseMatch) {
int lastStar = name.lastIndexOf('*');
if (lastStar > 1 && lastStar == name.length() - 1) {
if (name.charAt(lastStar - 1) != '*') {
pattern = wildcard.compilePattern(name.substring(0, lastStar - 1));
compiledPatterns.add(new Mapping<E>(name, pattern, target));
}
}
}
}
}
public void freeze() {
compiledPatterns = Collections.unmodifiableList(new ArrayList<Mapping<E>>());
}
/**
* <p> Matches the path against the compiled wildcard patterns. </p>
*
* @param potentialMatch The portion of the request URI for selecting a config.
* @return The action config if matched, else null
*/
public E match(String potentialMatch) {
E config = null;
if (compiledPatterns.size() > 0) {
log.debug("Attempting to match '{}' to a wildcard pattern, {} available", potentialMatch, compiledPatterns.size());
Map<String,String> vars = new LinkedHashMap<String,String>();
for (Mapping<E> m : compiledPatterns) {
if (wildcard.match(vars, potentialMatch, m.getPattern())) {
log.debug("Value matches pattern '{}'", m.getOriginalPattern());
config = convert(potentialMatch, m.getTarget(), vars);
break;
}
}
}
return config;
}
/**
* <p> Clones the target object and its children, replacing various
* properties with the values of the wildcard-matched strings. </p>
*
* @param path The requested path
* @param orig The original object
* @param vars A Map of wildcard-matched strings
* @return A cloned object with appropriate properties replaced with
* wildcard-matched values
*/
protected abstract E convert(String path, E orig, Map<String, String> vars);
/**
* <p>Replaces parameter values</p>
*
* @param orig The original parameters with placeholder values
* @param vars A Map of wildcard-matched strings
*
* @return map with replaced parameters
*/
protected Map<String,String> replaceParameters(Map<String, String> orig, Map<String,String> vars) {
Map<String, String> map = new LinkedHashMap<>();
//this will set the group index references, like {1}
for (Map.Entry<String,String> entry : orig.entrySet()) {
map.put(entry.getKey(), convertParam(entry.getValue(), vars));
}
if (appendNamedParameters) {
LOG.debug("Appending named parameters to the result map");
//the values map will contain entries like name->"Lex Luthor" and 1->"Lex Luthor"
//now add the non-numeric values
for (Map.Entry<String,String> entry: vars.entrySet()) {
if (!NumberUtils.isCreatable(entry.getKey())) {
map.put(entry.getKey(), entry.getValue());
}
}
}
return map;
}
/**
* <p> Inserts into a value wildcard-matched strings where specified
* with the {x} syntax. If a wildcard-matched value isn't found, the
* replacement token is turned into an empty string.
* </p>
*
* @param val The value to convert
* @param vars A Map of wildcard-matched strings
* @return The new value
*/
protected String convertParam(String val, Map<String, String> vars) {
if (val == null) {
return null;
}
int len = val.length();
StringBuilder ret = new StringBuilder();
char c;
String varVal;
for (int x=0; x<len; x++) {
c = val.charAt(x);
if (x < len - 2 &&
c == '{' && '}' == val.charAt(x+2)) {
varVal = (String)vars.get(String.valueOf(val.charAt(x + 1)));
if (varVal != null) {
ret.append(varVal);
}
x += 2;
} else {
ret.append(c);
}
}
return ret.toString();
}
/**
* <p> Stores a compiled wildcard pattern and the object it came
* from. </p>
*/
private static class Mapping<E> implements Serializable {
/**
* <p> The original pattern. </p>
*/
private String original;
/**
* <p> The compiled pattern. </p>
*/
private Object pattern;
/**
* <p> The original object. </p>
*/
private E config;
/**
* <p> Contructs a read-only Mapping instance. </p>
*
* @param original The original pattern
* @param pattern The compiled pattern
* @param config The original object
*/
public Mapping(String original, Object pattern, E config) {
this.original = original;
this.pattern = pattern;
this.config = config;
}
/**
* <p> Gets the compiled wildcard pattern. </p>
*
* @return The compiled pattern
*/
public Object getPattern() {
return this.pattern;
}
/**
* <p> Gets the object that contains the pattern. </p>
*
* @return The associated object
*/
public E getTarget() {
return this.config;
}
/**
* <p> Gets the original wildcard pattern. </p>
*
* @return The original pattern
*/
public String getOriginalPattern() {
return this.original;
}
}
}