| /* |
| * 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.Template; |
| 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)) { |
| |
| Template parentTemplate = getTemplate(); |
| |
| try { |
| FMParserTokenManager tkMan = new FMParserTokenManager( |
| new SimpleCharStream( |
| new StringReader(value), |
| beginLine, beginColumn + 1, |
| value.length())); |
| |
| FMParser parser = new FMParser(parentTemplate, false, tkMan, 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; |
| } |
| |
| } |