/* * 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; } }