blob: 91c429a872bf81b7ca009734841ccdc551be12b1 [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 freemarker.core;
import java.io.StringReader;
import java.util.List;
import freemarker.template.SimpleScalar;
import freemarker.template.TemplateException;
import freemarker.template.TemplateModel;
import freemarker.template.TemplateScalarModel;
import freemarker.template.utility.StringUtil;
final class StringLiteral extends Expression implements TemplateScalarModel {
private final String value;
/** {@link List} of {@link String}-s and {@link Interpolation}-s. */
private List<Object> dynamicValue;
StringLiteral(String value) {
this.value = value;
}
/**
* @param parentTkMan
* The token source of the template that contains this string literal. As of this writing, we only need
* this to share the {@code namingConvetion} with that.
*/
void parseValue(FMParserTokenManager parentTkMan, OutputFormat outputFormat) throws ParseException {
// The way this work is incorrect (the literal should be parsed without un-escaping),
// but we can't fix this backward compatibly.
if (value.length() > 3 && (value.indexOf("${") >= 0 || value.indexOf("#{") >= 0)) {
UnboundTemplate parentTemplate = getUnboundTemplate();
try {
FMParserTokenManager tkMan = new FMParserTokenManager(
new SimpleCharStream(
new StringReader(value),
beginLine, beginColumn + 1,
value.length()));
FMParser parser = new FMParser(parentTemplate, false, tkMan, null, parentTemplate.getParserConfiguration());
// We continue from the parent parser's current state:
parser.setupStringLiteralMode(parentTkMan, outputFormat);
try {
dynamicValue = parser.StaticTextAndInterpolations();
} finally {
// The parent parser continues from this parser's current state:
parser.tearDownStringLiteralMode(parentTkMan);
}
} catch (ParseException e) {
e.setTemplateName(parentTemplate.getSourceName());
throw e;
}
this.constantValue = null;
}
}
@Override
TemplateModel _eval(Environment env) throws TemplateException {
if (dynamicValue == null) {
return new SimpleScalar(value);
} else {
// This should behave like concatenating the values with `+`. Thus, an interpolated expression that
// returns markup promotes the result of the whole expression to markup.
// Exactly one of these is non-null, depending on if the result will be plain text or markup, which can
// change during evaluation, depending on the result of the interpolations:
StringBuilder plainTextResult = null;
TemplateMarkupOutputModel<?> markupResult = null;
for (Object part : dynamicValue) {
Object calcedPart =
part instanceof String ? part
: ((Interpolation) part).calculateInterpolatedStringOrMarkup(env);
if (markupResult != null) {
TemplateMarkupOutputModel<?> partMO = calcedPart instanceof String
? markupResult.getOutputFormat().fromPlainTextByEscaping((String) calcedPart)
: (TemplateMarkupOutputModel<?>) calcedPart;
markupResult = EvalUtil.concatMarkupOutputs(this, markupResult, partMO);
} else { // We are using `plainTextOutput` (or nothing yet)
if (calcedPart instanceof String) {
String partStr = (String) calcedPart;
if (plainTextResult == null) {
plainTextResult = new StringBuilder(partStr);
} else {
plainTextResult.append(partStr);
}
} else { // `calcedPart` is TemplateMarkupOutputModel
TemplateMarkupOutputModel<?> moPart = (TemplateMarkupOutputModel<?>) calcedPart;
if (plainTextResult != null) {
TemplateMarkupOutputModel<?> leftHandMO = moPart.getOutputFormat()
.fromPlainTextByEscaping(plainTextResult.toString());
markupResult = EvalUtil.concatMarkupOutputs(this, leftHandMO, moPart);
plainTextResult = null;
} else {
markupResult = moPart;
}
}
}
} // for each part
return markupResult != null ? markupResult
: plainTextResult != null ? new SimpleScalar(plainTextResult.toString())
: SimpleScalar.EMPTY_STRING;
}
}
public String getAsString() {
return value;
}
/**
* Tells if this is something like <tt>"${foo}"</tt>, which is usually a user mistake.
*/
boolean isSingleInterpolationLiteral() {
return dynamicValue != null && dynamicValue.size() == 1
&& dynamicValue.get(0) instanceof Interpolation;
}
@Override
public String getCanonicalForm() {
if (dynamicValue == null) {
return StringUtil.ftlQuote(value);
} else {
StringBuilder sb = new StringBuilder();
sb.append('"');
for (Object child : dynamicValue) {
if (child instanceof Interpolation) {
sb.append(((Interpolation) child).getCanonicalFormInStringLiteral());
} else {
sb.append(StringUtil.FTLStringLiteralEnc((String) child, '"'));
}
}
sb.append('"');
return sb.toString();
}
}
@Override
String getNodeTypeSymbol() {
return dynamicValue == null ? getCanonicalForm() : "dynamic \"...\"";
}
@Override
boolean isLiteral() {
return dynamicValue == null;
}
@Override
protected Expression deepCloneWithIdentifierReplaced_inner(
String replacedIdentifier, Expression replacement, ReplacemenetState replacementState) {
StringLiteral cloned = new StringLiteral(value);
// FIXME: replacedIdentifier should be searched inside interpolatedOutput too:
cloned.dynamicValue = this.dynamicValue;
return cloned;
}
@Override
int getParameterCount() {
return dynamicValue == null ? 0 : dynamicValue.size();
}
@Override
Object getParameterValue(int idx) {
checkIndex(idx);
return dynamicValue.get(idx);
}
private void checkIndex(int idx) {
if (dynamicValue == null || idx >= dynamicValue.size()) {
throw new IndexOutOfBoundsException();
}
}
@Override
ParameterRole getParameterRole(int idx) {
checkIndex(idx);
return ParameterRole.VALUE_PART;
}
}