/*
* Copyright 2014 Martin Kouba
*
* Licensed 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.trimou.handlebars;
import static org.trimou.handlebars.OptionsHashKeys.ELSE;
import static org.trimou.handlebars.OptionsHashKeys.LOGIC;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.trimou.engine.config.EngineConfigurationKey;
import org.trimou.util.ImmutableSet;
import org.trimou.util.Strings;
/**
* An abstract helper which renders a block if:
* <ul>
* <li>there are no params and the object at the top of the context stack
* matches, or</li>
* <li>the parameters match according to the current evaluation logic</li>
* </ul>
*
* <p>
* An optional <code>else</code> may be specified. If not a string literal
* {@link Object#toString()} is used. The final string may contain simple value
* expressions (evaluated in the same way as helper params). The default
* delimiters are <code>{</code> and <code>}</code>. Note that for string
* literals it's not possible to use the current template delimiters, i.e.
* <code>{{</code> and <code>}}</code>.
* </p>
*
* <p>
* Don't make this class public until we have a proper name.
* </p>
*
* @author Martin Kouba
* @see IfHelper
* @see UnlessHelper
*/
abstract class MatchingSectionHelper extends BasicSectionHelper {
private static final Logger LOGGER = LoggerFactory
.getLogger(MatchingSectionHelper.class);
private final String elseStartDelimiter;
private final Pattern elsePattern;
MatchingSectionHelper() {
this(EngineConfigurationKey.START_DELIMITER.getDefaultValue().toString()
.substring(0, 1),
EngineConfigurationKey.END_DELIMITER.getDefaultValue()
.toString().substring(0, 1));
}
MatchingSectionHelper(String elseStartDelimiter, String elseEndDelimiter) {
this.elseStartDelimiter = elseStartDelimiter;
this.elsePattern = initElsePattern(elseStartDelimiter,
elseEndDelimiter);
}
@Override
public void execute(Options options) {
if ((options.getParameters().isEmpty() && isMatching(options.peek(), options))
|| matches(options)) {
options.fn();
} else {
Object elseBlock = options.getHash().get(ELSE);
if (elseBlock != null) {
String elseString = elseBlock.toString();
if (elseString.contains(elseStartDelimiter)) {
append(options, interpolate(elseString, options));
} else {
append(options, elseString);
}
}
}
}
@Override
protected Set<String> getSupportedHashKeys() {
return ImmutableSet.of(LOGIC, ELSE);
}
protected EvaluationLogic getDefaultLogic() {
return EvaluationLogic.AND;
}
protected abstract boolean isMatching(Object value);
protected boolean isMatching(Object value, Options options) {
return isMatching(value);
}
protected boolean hasEmptyParamsSupport() {
return false;
}
@Override
protected int numberOfRequiredParameters() {
return hasEmptyParamsSupport() ? 0 : super.numberOfRequiredParameters();
}
protected enum EvaluationLogic {
// At least one of the params must match
OR {
Boolean test(boolean isMatching) {
return isMatching ? true : null;
}
boolean defaultResult() {
return false;
}
},
// All the params must match
AND {
Boolean test(boolean isMatching) {
return !isMatching ? false : null;
}
boolean defaultResult() {
return true;
}
},;
/**
* @param isParamMatching
* @return a non-null value if other params do not need to be evaluated,
* the return value is the result of the evaluation
*/
abstract Boolean test(boolean isParamMatching);
abstract boolean defaultResult();
public static EvaluationLogic parse(String value) {
for (EvaluationLogic logic : values()) {
if (value.equalsIgnoreCase(logic.toString())) {
return logic;
}
}
return null;
}
}
private boolean matches(Options options) {
// Very often there is only one param
List<Object> params = options.getParameters();
if (params.size() == 1) {
return isMatching(params.get(0), options);
}
EvaluationLogic logic = getLogic(options.getHash());
for (Object param : params) {
Boolean value = logic.test(isMatching(param, options));
if (value != null) {
return value;
}
}
return logic.defaultResult();
}
private EvaluationLogic getLogic(Map<String, Object> hash) {
if (hash.isEmpty() || !hash.containsKey(LOGIC)) {
return getDefaultLogic();
}
String customLogic = hash.get(LOGIC).toString();
EvaluationLogic logic = EvaluationLogic.parse(customLogic);
if (logic == null) {
LOGGER.warn(
"Unsupported evaluation logic specified: {}, using the default one: {}",
customLogic, getDefaultLogic());
logic = getDefaultLogic();
}
return logic;
}
private String interpolate(String elseString, Options options) {
StringBuffer result = new StringBuffer();
Matcher matcher = elsePattern.matcher(elseString);
while (matcher.find()) {
Object value = options.getValue(matcher.group(2).trim());
String replacement = value != null ? value.toString()
: Strings.EMPTY;
matcher.appendReplacement(result, replacement);
}
matcher.appendTail(result);
return result.toString();
}
static Pattern initElsePattern(String elseStartDelimiter,
String elseEndDelimiter) {
return Pattern.compile("(" + Pattern.quote(elseStartDelimiter)
+ ")(.*?)(" + Pattern.quote(elseEndDelimiter) + ")");
}
}