blob: b0addadddd8ab5bce709ec8edaa3086dbaabd3e5 [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.util.ArrayList;
import java.util.Collections;
import freemarker.template.SimpleScalar;
import freemarker.template.SimpleSequence;
import freemarker.template.TemplateException;
import freemarker.template.TemplateHashModel;
import freemarker.template.TemplateModel;
import freemarker.template.TemplateNumberModel;
import freemarker.template.TemplateScalarModel;
import freemarker.template.TemplateSequenceModel;
import freemarker.template._TemplateAPI;
import freemarker.template.utility.Constants;
/**
* {@code target[keyExpression]}, where, in FM 2.3, {@code keyExpression} can be string, a number or a range,
* and {@code target} can be a hash or a sequence.
*/
final class DynamicKeyName extends Expression {
private final Expression keyExpression;
private final Expression target;
DynamicKeyName(Expression target, Expression keyExpression) {
this.target = target;
this.keyExpression = keyExpression;
}
@Override
TemplateModel _eval(Environment env) throws TemplateException {
TemplateModel targetModel = target.eval(env);
if (targetModel == null) {
if (env.isClassicCompatible()) {
return null;
} else {
throw InvalidReferenceException.getInstance(target, env);
}
}
TemplateModel keyModel = keyExpression.eval(env);
if (keyModel == null) {
if (env.isClassicCompatible()) {
keyModel = TemplateScalarModel.EMPTY_STRING;
} else {
keyExpression.assertNonNull(null, env);
}
}
if (keyModel instanceof TemplateNumberModel) {
int index = keyExpression.modelToNumber(keyModel, env).intValue();
return dealWithNumericalKey(targetModel, index, env);
}
if (keyModel instanceof TemplateScalarModel) {
String key = EvalUtil.modelToString((TemplateScalarModel) keyModel, keyExpression, env);
return dealWithStringKey(targetModel, key, env);
}
if (keyModel instanceof RangeModel) {
return dealWithRangeKey(targetModel, (RangeModel) keyModel, env);
}
throw new UnexpectedTypeException(keyExpression, keyModel, "number, range, or string",
new Class[] { TemplateNumberModel.class, TemplateScalarModel.class, Range.class }, env);
}
static private Class[] NUMERICAL_KEY_LHO_EXPECTED_TYPES;
static {
NUMERICAL_KEY_LHO_EXPECTED_TYPES = new Class[1 + NonStringException.STRING_COERCABLE_TYPES.length];
NUMERICAL_KEY_LHO_EXPECTED_TYPES[0] = TemplateSequenceModel.class;
for (int i = 0; i < NonStringException.STRING_COERCABLE_TYPES.length; i++) {
NUMERICAL_KEY_LHO_EXPECTED_TYPES[i + 1] = NonStringException.STRING_COERCABLE_TYPES[i];
}
}
private TemplateModel dealWithNumericalKey(TemplateModel targetModel,
int index,
Environment env)
throws TemplateException {
if (targetModel instanceof TemplateSequenceModel) {
TemplateSequenceModel tsm = (TemplateSequenceModel) targetModel;
int size;
try {
size = tsm.size();
} catch (Exception e) {
size = Integer.MAX_VALUE;
}
return index < size ? tsm.get(index) : null;
}
try {
String s = target.evalAndCoerceToPlainText(env);
try {
return new SimpleScalar(s.substring(index, index + 1));
} catch (IndexOutOfBoundsException e) {
if (index < 0) {
throw new _MiscTemplateException("Negative index not allowed: ", Integer.valueOf(index));
}
if (index >= s.length()) {
throw new _MiscTemplateException(
"String index out of range: The index was ", Integer.valueOf(index),
" (0-based), but the length of the string is only ", Integer.valueOf(s.length()) , ".");
}
throw new RuntimeException("Can't explain exception", e);
}
} catch (NonStringException e) {
throw new UnexpectedTypeException(
target, targetModel,
"sequence or " + NonStringException.STRING_COERCABLE_TYPES_DESC,
NUMERICAL_KEY_LHO_EXPECTED_TYPES,
(targetModel instanceof TemplateHashModel
? "You had a numberical value inside the []. Currently that's only supported for "
+ "sequences (lists) and strings. To get a Map item with a non-string key, "
+ "use myMap?api.get(myKey)."
: null),
env);
}
}
private TemplateModel dealWithStringKey(TemplateModel targetModel, String key, Environment env)
throws TemplateException {
if (targetModel instanceof TemplateHashModel) {
return((TemplateHashModel) targetModel).get(key);
}
throw new NonHashException(target, targetModel, env);
}
private TemplateModel dealWithRangeKey(TemplateModel targetModel, RangeModel range, Environment env)
throws UnexpectedTypeException, InvalidReferenceException, TemplateException {
final TemplateSequenceModel targetSeq;
final String targetStr;
if (targetModel instanceof TemplateSequenceModel) {
targetSeq = (TemplateSequenceModel) targetModel;
targetStr = null;
} else {
targetSeq = null;
try {
targetStr = target.evalAndCoerceToPlainText(env);
} catch (NonStringException e) {
throw new UnexpectedTypeException(
target, target.eval(env),
"sequence or " + NonStringException.STRING_COERCABLE_TYPES_DESC,
NUMERICAL_KEY_LHO_EXPECTED_TYPES, env);
}
}
final int size = range.size();
final boolean rightUnbounded = range.isRightUnbounded();
final boolean rightAdaptive = range.isRightAdaptive();
// Right bounded empty ranges are accepted even if the begin index is out of bounds. That's because a such range
// produces an empty sequence, which thus doesn't contain any illegal indexes.
if (!rightUnbounded && size == 0) {
return emptyResult(targetSeq != null);
}
final int firstIdx = range.getBegining();
if (firstIdx < 0) {
throw new _MiscTemplateException(keyExpression,
"Negative range start index (", Integer.valueOf(firstIdx),
") isn't allowed for a range used for slicing.");
}
final int targetSize = targetStr != null ? targetStr.length() : targetSeq.size();
final int step = range.getStep();
// Right-adaptive increasing ranges can start 1 after the last element of the target, because they are like
// ranges with exclusive end index of at most targetSize. Thence a such range is just an empty list of indexes,
// and thus it isn't out-of-bounds.
// Right-adaptive decreasing ranges has exclusive end -1, so it can't help on a to high firstIndex.
// Right-bounded ranges at this point aren't empty, so the right index surely can't reach targetSize.
if (rightAdaptive && step == 1 ? firstIdx > targetSize : firstIdx >= targetSize) {
throw new _MiscTemplateException(keyExpression,
"Range start index ", Integer.valueOf(firstIdx), " is out of bounds, because the sliced ",
(targetStr != null ? "string" : "sequence"),
" has only ", Integer.valueOf(targetSize), " ", (targetStr != null ? "character(s)" : "element(s)"),
". ", "(Note that indices are 0-based).");
}
final int resultSize;
if (!rightUnbounded) {
final int lastIdx = firstIdx + (size - 1) * step;
if (lastIdx < 0) {
if (!rightAdaptive) {
throw new _MiscTemplateException(keyExpression,
"Negative range end index (", Integer.valueOf(lastIdx),
") isn't allowed for a range used for slicing.");
} else {
resultSize = firstIdx + 1;
}
} else if (lastIdx >= targetSize) {
if (!rightAdaptive) {
throw new _MiscTemplateException(keyExpression,
"Range end index ", Integer.valueOf(lastIdx), " is out of bounds, because the sliced ",
(targetStr != null ? "string" : "sequence"),
" has only ", Integer.valueOf(targetSize), " ", (targetStr != null ? "character(s)" : "element(s)"),
". (Note that indices are 0-based).");
} else {
resultSize = Math.abs(targetSize - firstIdx);
}
} else {
resultSize = size;
}
} else {
resultSize = targetSize - firstIdx;
}
if (resultSize == 0) {
return emptyResult(targetSeq != null);
}
if (targetSeq != null) {
ArrayList/*<TemplateModel>*/ list = new ArrayList(resultSize);
int srcIdx = firstIdx;
for (int i = 0; i < resultSize; i++) {
list.add(targetSeq.get(srcIdx));
srcIdx += step;
}
// List items are already wrapped, so the wrapper will be null:
return new SimpleSequence(list, null);
} else {
final int exclEndIdx;
if (step < 0 && resultSize > 1) {
if (!(range.isAffactedByStringSlicingBug() && resultSize == 2)) {
throw new _MiscTemplateException(keyExpression,
"Decreasing ranges aren't allowed for slicing strings (as it would give reversed text). "
+ "The index range was: first = ", Integer.valueOf(firstIdx),
", last = ", Integer.valueOf(firstIdx + (resultSize - 1) * step));
} else {
// Emulate the legacy bug, where "foo"[n .. n-1] gives "" instead of an error (if n >= 1).
// Fix this in FTL [2.4]
exclEndIdx = firstIdx;
}
} else {
exclEndIdx = firstIdx + resultSize;
}
return new SimpleScalar(targetStr.substring(firstIdx, exclEndIdx));
}
}
private TemplateModel emptyResult(boolean seq) {
return seq
? (_TemplateAPI.getTemplateLanguageVersionAsInt(this) < _TemplateAPI.VERSION_INT_2_3_21
? new SimpleSequence(Collections.EMPTY_LIST, null)
: Constants.EMPTY_SEQUENCE)
: TemplateScalarModel.EMPTY_STRING;
}
@Override
public String getCanonicalForm() {
return target.getCanonicalForm()
+ "["
+ keyExpression.getCanonicalForm()
+ "]";
}
@Override
String getNodeTypeSymbol() {
return "...[...]";
}
@Override
boolean isLiteral() {
return constantValue != null || (target.isLiteral() && keyExpression.isLiteral());
}
@Override
int getParameterCount() {
return 2;
}
@Override
Object getParameterValue(int idx) {
return idx == 0 ? target : keyExpression;
}
@Override
ParameterRole getParameterRole(int idx) {
return idx == 0 ? ParameterRole.LEFT_HAND_OPERAND : ParameterRole.ENCLOSED_OPERAND;
}
@Override
protected Expression deepCloneWithIdentifierReplaced_inner(
String replacedIdentifier, Expression replacement, ReplacemenetState replacementState) {
return new DynamicKeyName(
target.deepCloneWithIdentifierReplaced(replacedIdentifier, replacement, replacementState),
keyExpression.deepCloneWithIdentifierReplaced(replacedIdentifier, replacement, replacementState));
}
}