/*
* Copyright 2008 Google Inc.
*
* 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 com.google.template.soy.soytree;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Maps;
import com.google.template.soy.base.SourceLocation;
import com.google.template.soy.error.ErrorReporter;
import com.google.template.soy.error.SoyErrorKind;
import com.google.template.soy.exprparse.SoyParsingContext;
import java.util.Collection;
import java.util.Map;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* A class for parsing attributes out of command text.
*
*/
public final class CommandTextAttributesParser {
public static final SoyErrorKind MALFORMED_ATTRIBUTES =
SoyErrorKind.of("Malformed attributes in ''{0}'' command text ({1}).");
public static final SoyErrorKind UNSUPPORTED_ATTRIBUTE =
SoyErrorKind.of("Unsupported attribute ''{0}'' in ''{1}'' command text ({2}).");
private static final SoyErrorKind DUPLICATE_ATTRIBUTE =
SoyErrorKind.of("Duplicate attribute ''{0}'' in ''{1}'' command text ({2}).");
private static final SoyErrorKind INVALID_ATTRIBUTE_VALUE =
SoyErrorKind.of(
"Invalid value for attribute ''{0}'' in ''{1}'' command text ({2}). "
+ "Valid values are {3}.");
private static final SoyErrorKind MISSING_REQUIRED_ATTRIBUTE =
SoyErrorKind.of("Missing required attribute ''{0}'' in ''{1}'' command text ({2}).");
/**
* Regex pattern for an attribute in command text. Note group(1) is attribute name, group(2) is
* attribute value. E.g. data="$boo" parses into group(1)="data" and group(2)="$boo".
*/
private static final Pattern ATTRIBUTE_TEXT =
Pattern.compile(
"([a-zA-Z][a-zA-Z0-9-]*) \\s* = \\s* \" ( (?:[^\"\\\\]+ | \\\\.)*+ ) \" \\s*",
Pattern.COMMENTS | Pattern.DOTALL);
/**
* Regexp pattern to unescape attribute values. group(1) holds the escaped character. The
* backslash used for escaping is removed only if it is followed by a backslash or a quote. This
* is to support templates created before introduction of escaping. a\b\c becomes a\b\c.
*/
private static final Pattern ATTRIBUTE_VALUE_ESCAPE = Pattern.compile("\\\\([\"\\\\])");
/** The name of the Soy command handled by this parser. Only used in error messages. */
private final String commandName;
/** The set of this parser's supported attributes. */
private final Set<Attribute> supportedAttributes;
/** The set of names of the supported attributes. */
private final Set<String> supportedAttributeNames;
/**
* @param commandName The name of the Soy command that this parser handles. Only used in
* generating error messages when an exception is thrown.
* @param supportedAttributes This parser's supported attributes.
*/
CommandTextAttributesParser(String commandName, Attribute... supportedAttributes) {
this.commandName = commandName;
this.supportedAttributes = ImmutableSet.copyOf(supportedAttributes);
ImmutableSet.Builder<String> supportedAttributeNamesBuilder = ImmutableSet.builder();
for (Attribute attribute : supportedAttributes) {
supportedAttributeNamesBuilder.add(attribute.name);
// Sanity check that the default values are allowed values.
if (attribute.allowedValues == Attribute.ALLOW_ALL_VALUES
|| attribute.defaultValue == null
|| Attribute.NO_DEFAULT_VALUE_BECAUSE_REQUIRED.equals(attribute.defaultValue)) {
continue; // nothing to check
}
Preconditions.checkArgument(attribute.allowedValues.contains(attribute.defaultValue));
}
supportedAttributeNames = supportedAttributeNamesBuilder.build();
}
/**
* Parses a command text string into a map of attributes names to values. The command text is
* assumed to be for the Soy command that this parser handles.
*
* @param commandText The command text to parse.
* @param context For reporting syntax errors.
* @param sourceLocation A source location near the command text, for producing useful error
* reports.
* @return A map from attribute names to values.
*/
Map<String, String> parse(
String commandText, SoyParsingContext context, SourceLocation sourceLocation) {
return parse(commandText, context.errorReporter(), sourceLocation);
}
/**
* Parses a command text string into a map of attributes names to values. The command text is
* assumed to be for the Soy command that this parser handles.
*
* @param commandText The command text to parse.
* @param errorReporter For reporting syntax errors.
* @param sourceLocation A source location near the command text, for producing useful error
* reports.
* @return A map from attribute names to values.
*/
Map<String, String> parse(
String commandText, ErrorReporter errorReporter, SourceLocation sourceLocation) {
Map<String, String> attributes = Maps.newHashMap();
// --- Parse the attributes ---
int i = 0; // index in commandText that we've processed up to
Matcher matcher = ATTRIBUTE_TEXT.matcher(commandText);
while (matcher.find(i)) {
if (matcher.start() != i) {
errorReporter.report(sourceLocation, MALFORMED_ATTRIBUTES, commandName, commandText);
}
i = matcher.end();
String name = matcher.group(1);
String value = matcher.group(2);
value = ATTRIBUTE_VALUE_ESCAPE.matcher(value).replaceAll("$1");
if (!supportedAttributeNames.contains(name)) {
errorReporter.report(sourceLocation, UNSUPPORTED_ATTRIBUTE, name, commandName, commandText);
}
if (attributes.containsKey(name)) {
errorReporter.report(sourceLocation, DUPLICATE_ATTRIBUTE, name, commandName, commandText);
}
attributes.put(name, value);
}
if (i != commandText.length()) {
errorReporter.report(sourceLocation, MALFORMED_ATTRIBUTES, commandName, commandText);
}
// --- Apply default values or check correctness of supplied values ---
for (Attribute supportedAttribute : supportedAttributes) {
if (attributes.containsKey(supportedAttribute.name)) {
// Check that the supplied value is allowed.
if (supportedAttribute.allowedValues == Attribute.ALLOW_ALL_VALUES) {
continue; // nothing to check
}
if (!supportedAttribute.allowedValues.contains(attributes.get(supportedAttribute.name))) {
errorReporter.report(
sourceLocation,
INVALID_ATTRIBUTE_VALUE,
supportedAttribute.name,
commandName,
commandText,
supportedAttribute.allowedValues.toString());
}
} else {
// Check that the attribute is not required.
if (Attribute.NO_DEFAULT_VALUE_BECAUSE_REQUIRED.equals(supportedAttribute.defaultValue)) {
errorReporter.report(
sourceLocation,
MISSING_REQUIRED_ATTRIBUTE,
supportedAttribute.name,
commandName,
commandText);
}
// Apply default value.
attributes.put(supportedAttribute.name, supportedAttribute.defaultValue);
}
}
return attributes;
}
// -----------------------------------------------------------------------------------------------
// Attribute record.
/** Record for a supported attribute. */
public static class Attribute {
/** Use this as the allowed values set when there is no fixed set of allowed values. */
public static final Collection<String> ALLOW_ALL_VALUES = null;
/** Use this as the allowed values set for a boolean attribute. */
public static final ImmutableSet<String> BOOLEAN_VALUES = ImmutableSet.of("true", "false");
/**
* Use this as the default attribute value when there should not be a default because the
* attribute is required. (Non-required attributes must have default values.)
*/
public static final String NO_DEFAULT_VALUE_BECAUSE_REQUIRED = "__NDVBR__";
/** The attribute name. */
final String name;
/**
* The collection of allowed values, or {@link #ALLOW_ALL_VALUES} if there's no fixed set of
* allowed values.
*/
final Collection<String> allowedValues;
/**
* The default value, or {@link #NO_DEFAULT_VALUE_BECAUSE_REQUIRED} if the attribute is
* required.
*/
final String defaultValue;
/**
* The definition of one supported attribute. If there is no fixed set of allowed values, use
* {@link #ALLOW_ALL_VALUES}. Non-required attributes must have default values. Required
* dattributes should map to a default value of {@link #NO_DEFAULT_VALUE_BECAUSE_REQUIRED}.
*
* @param name The attribute name.
* @param allowedValues The collection of allowed values, or {@link #ALLOW_ALL_VALUES} if
* there's no fixed set of allowed values.
* @param defaultValue The default value, or {@link #NO_DEFAULT_VALUE_BECAUSE_REQUIRED} if the
* attribute is required.
*/
public Attribute(String name, Collection<String> allowedValues, String defaultValue) {
this.name = name;
this.allowedValues = allowedValues;
this.defaultValue = defaultValue;
}
}
}