/*
* 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.BREAK;
import java.util.HashSet;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.trimou.engine.MustacheTagInfo;
import org.trimou.engine.MustacheTagType;
import org.trimou.util.ImmutableSet;
import org.trimou.util.Nested;
/**
* This helper works similarly as the Java switch statement.
*
* <p>
* This helper should only contain <b>case</b> and <b>default</b> sections. Other types of
* segments are always rendered. Note that the <b>default</b> section always
* terminates the flow!
* </p>
*
* <p>
* Since we push the flow object on the context stack, it's possible to refer
* the value in the switch expression with {@link Nested#up()} method. See also
* examples.
* </p>
*
* By default, the following template will render "Hello world!":
*
* <pre>
* {{#switch "hello"}}
* {{#case "foo" break="true"}}
* Hello foo!
* {{/case}}
* {{#case "hello" break="true"}}
* Hello world!
* {{/case}}
* {{#default}}
* No case for {{this.up}}.
* {{/default}}
* {{/switch}}
* </pre>
*
* By default, the following template will render
* "Hello world! No case for hello.":
*
* <pre>
* {{#switch "hello"}}
* {{#case "foo"}}
* Hello foo!
* {{/case}}
* {{#case "hello"}}
* Hello world!
* {{/case}}
* {{#default}}
* No case for {{this.up}}.
* {{/default}}
* {{/switch}}
* </pre>
*
*
* @author Martin Kouba
* @see HelpersBuilder#addSwitch()
*/
public class SwitchHelper extends BasicSectionHelper {
private static final Logger LOGGER = LoggerFactory
.getLogger(SwitchHelper.class);
@Override
protected int numberOfRequiredParameters() {
return 0;
}
@Override
public void execute(Options options) {
// Take the first param or the current context object
Object value = options.getParameters().isEmpty() ? options.peek()
: options.getParameters().get(0);
if (value == null) {
// Don't match a null value
return;
}
options.push(new Flow(value));
options.fn();
options.pop();
}
@Override
public void validate(HelperDefinition definition) {
super.validate(definition);
Set<String> validNames = new HashSet<>(4);
for (Entry<String, Helper> entry : configuration.getHelpers()
.entrySet()) {
if (entry.getValue() instanceof CaseHelper
|| entry.getValue() instanceof DefaultHelper) {
validNames.add(entry.getKey());
}
}
for (MustacheTagInfo info : definition.getTagInfo().getChildTags()) {
if (!isValid(info, validNames)) {
LOGGER.warn(
"Invalid content detected {}. This helper should only contain case and default sections. Other types of segments are always rendered!",
info);
}
}
}
private boolean isValid(MustacheTagInfo info, Set<String> validNames) {
if (!info.getType().equals(MustacheTagType.SECTION)) {
return false;
}
for (String name : validNames) {
if (info.getText().startsWith(name)) {
return true;
}
}
return false;
}
/**
* The first param is the matching value. By default, the flow is only
* terminated if a key <code>break</code> has the value of true.
*
* @author Martin Kouba
*/
public static class CaseHelper extends BasicSectionHelper {
private final boolean defaultIsBreak;
/**
*
*/
public CaseHelper() {
this(false);
}
/**
*
* @param defaultIsBreak
* If <code>true</code> the matching case helper terminates
* the flow by default.
*/
public CaseHelper(boolean defaultIsBreak) {
this.defaultIsBreak = defaultIsBreak;
}
@Override
public void execute(Options options) {
Object contextObject = options.peek();
if (contextObject instanceof Flow) {
Flow flow = (Flow) contextObject;
if (!flow.isTerminated()) {
if (flow.isFallThrough()
|| flow.up().equals(options.getParameters().get(0))) {
options.fn();
flow.setFallThrough();
if (isBreak(options.getHash())) {
flow.terminate();
}
}
}
} else {
throw Flow.newInvalidFlowException(options.getTagInfo());
}
}
@Override
protected Set<String> getSupportedHashKeys() {
return ImmutableSet.of(BREAK);
}
private boolean isBreak(Map<String, Object> hash) {
if (hash.isEmpty() || !hash.containsKey(BREAK)) {
return defaultIsBreak;
}
return Boolean.valueOf(hash.get(BREAK).toString());
}
}
public static class DefaultHelper extends BasicSectionHelper {
@Override
protected int numberOfRequiredParameters() {
return 0;
}
@Override
public void execute(Options options) {
Object contextObject = options.peek();
if (contextObject instanceof Flow) {
Flow flow = (Flow) contextObject;
if (!flow.isTerminated()) {
options.fn();
// Always terminate the flow
flow.terminate();
}
} else {
throw Flow.newInvalidFlowException(options.getTagInfo());
}
}
}
}